Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions .github/workflows/build-binaries.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
name: Build Native Binaries

on:
push:
tags:
- "v*"
workflow_dispatch:

jobs:
build:
name: Build (${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
artifact_name: passifypdf-linux-x86_64
binary_path: dist/passifypdf
- os: macos-latest

Copilot AI Feb 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The matrix labels the macOS artifact as arm64, but macos-latest isn't guaranteed to stay on Apple Silicon. Pin the runner to an explicit version/arch (e.g., macos-14 for arm64) or rename the artifact to avoid claiming a specific architecture when it's not enforced.

Suggested change
- os: macos-latest
- os: macos-14

Copilot uses AI. Check for mistakes.
artifact_name: passifypdf-macos-arm64
binary_path: dist/passifypdf
- os: windows-latest
artifact_name: passifypdf-windows-x86_64
binary_path: dist/passifypdf.exe

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"

- name: Install uv
uses: astral-sh/setup-uv@v4

- name: Install dependencies
run: uv sync

- name: Install PyInstaller
run: uv pip install pyinstaller

- name: Build binary
run: |
uv run pyinstaller \
Comment on lines +36 to +47

Copilot AI Feb 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This workflow uses uv sync, but the repo's dependency management is Poetry-based (see .github/workflows/ci.yml and pyproject.toml). As-is, uv sync depends on uv.lock, which currently doesn't list any dependencies (e.g., pypdf), so the build environment may be missing runtime deps needed by PyInstaller. Consider either switching this workflow to poetry install (matching CI) or generating/committing a complete uv.lock produced by uv lock so uv sync installs the same deps as Poetry.

Suggested change
- name: Install uv
uses: astral-sh/setup-uv@v4
- name: Install dependencies
run: uv sync
- name: Install PyInstaller
run: uv pip install pyinstaller
- name: Build binary
run: |
uv run pyinstaller \
- name: Install Poetry
run: |
python -m pip install --upgrade pip
python -m pip install poetry
- name: Install dependencies
run: |
poetry install --no-interaction --no-ansi
- name: Install PyInstaller
run: |
poetry run python -m pip install pyinstaller
- name: Build binary
run: |
poetry run pyinstaller \

Copilot uses AI. Check for mistakes.
--onefile \
--name passifypdf \
--console \
passifypdf/encryptpdf.py

Copilot AI Feb 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PyInstaller is being pointed at passifypdf/encryptpdf.py, but that file uses a relative import (from .cli import get_arg_parser). When executed as a script (which is how the frozen app runs the entry module), that relative import will fail. Use an entry point that imports the package module (e.g., a small top-level main.py that calls passifypdf.encryptpdf.main, or a passifypdf/__main__.py) and build from that instead.

Suggested change
passifypdf/encryptpdf.py
-m passifypdf.encryptpdf

Copilot uses AI. Check for mistakes.

- name: Smoke test (Unix)
if: runner.os != 'Windows'
run: |
./dist/passifypdf --help

- name: Smoke test (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
.\dist\passifypdf.exe --help

- name: Upload binary artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact_name }}
path: ${{ matrix.binary_path }}
if-no-files-found: error

release:
name: Attach Binaries to Release
needs: build
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v')
permissions:
contents: write

steps:
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: dist/

- name: List downloaded artifacts
run: ls -R dist/

- name: Create release and upload assets
uses: softprops/action-gh-release@v2
with:
files: |
dist/passifypdf-linux-x86_64/passifypdf
dist/passifypdf-macos-arm64/passifypdf
dist/passifypdf-windows-x86_64/passifypdf.exe
generate_release_notes: true
26 changes: 22 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,27 @@ passifypdf -i input.pdf -o protected.pdf -p mySecretPassword
```

## Known Issues
If you have nay special chars(example: an emoji like Star 🌟) in the PDF file, it gives a minor complain during execution.
But it still does the job, so you can ingore that "char or object error" which you see in the output.
If you have any special chars (example: an emoji like Star 🌟) in the PDF file, it gives a minor complaint during execution.
But it still does the job, so you can ignore that "char or object error" which you see in the output.

## Web UI (Streamlit)

A local web interface is available for users who prefer a graphical workflow:

```bash
# Install web UI dependencies
pip install -r requirements-webui.txt

# Launch the app
streamlit run app/streamlit_app.py
```
Comment thread
nirmalchandra marked this conversation as resolved.

Open the URL shown in your terminal (usually `http://localhost:8501`), upload a PDF, enter a password and download the protected file — no command line needed.

## Download Pre-built Binaries

Standalone executables for Linux, macOS, and Windows are built automatically on every tagged release via GitHub Actions. Visit the [Releases page](https://github.com/SUPAIDEAS/passifypdf/releases) to download the binary for your platform — no Python installation required.

## Note:
In general you can use passifypdf to protect your PDF files against chance attackers.
But you should not rely on this for mission-critical data or situation.
In general you can use passifypdf to protect your PDF files against chance attackers.
But you should not rely on this for mission-critical data or situations.
16 changes: 16 additions & 0 deletions UI-README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
Use the following CLI commands to launch the UI.
- UI allows you to drag-drop PDF file, encrypt it(password protect it) and then, download it.

Commands to run:
```shell
uv sync --all-groups
uv pip install -r requirements-webui.txt
uv pip install -e . <---------- Important, otherwise it gives import error.
uv run streamlit run app/streamlit_app.py
```
Then,
Open:
http://localhost:8501/

Screenshot:
<img width="1112" height="839" alt="image" src="https://github.com/user-attachments/assets/a5fdbe30-052e-4176-8abc-21a8c589f557" />
118 changes: 118 additions & 0 deletions app/streamlit_app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
"""Streamlit web UI wrapper for passifypdf.

Run locally with:
streamlit run app/streamlit_app.py
"""

import io

Copilot AI Feb 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

io is imported but never used. Please remove it to avoid lint/quality noise.

Suggested change
import io

Copilot uses AI. Check for mistakes.
import tempfile
from pathlib import Path

import streamlit as st

from passifypdf.encryptpdf import encrypt_pdf


# ── Page config ────────────────────────────────────────────────────────────

st.set_page_config(
page_title="passifypdf — PDF Password Protector",
page_icon="🔒",
layout="centered",
initial_sidebar_state="collapsed",
)

# ── Title ──────────────────────────────────────────────────────────────────

st.title("🔒 passifypdf")
st.subheader("Protect your PDF files with a password — right in the browser.")
st.markdown("---")

# ── File upload ────────────────────────────────────────────────────────────

uploaded_file = st.file_uploader(
"Upload a PDF file",
type=["pdf"],
help="Select the PDF you want to encrypt.",
)

# ── Password ───────────────────────────────────────────────────────────────

col1, col2 = st.columns(2)
with col1:
password = st.text_input(
"Password",
type="password",
placeholder="Enter a strong password",
help="This password will be required to open the encrypted PDF.",
)
with col2:
confirm_password = st.text_input(
"Confirm password",
type="password",
placeholder="Re-enter the password",
)

# ── Output filename ────────────────────────────────────────────────────────

output_name = st.text_input(
"Output filename",
value="protected.pdf",
help="Name of the encrypted file you will download.",
)
if not output_name.lower().endswith(".pdf"):
output_name += ".pdf"

# ── Encrypt button ─────────────────────────────────────────────────────────

if st.button("🔐 Encrypt PDF", type="primary", use_container_width=True):
if not uploaded_file:
st.error("Please upload a PDF file first.")
elif not password:
st.error("Please enter a password.")
elif password != confirm_password:
st.error("Passwords do not match. Please try again.")
else:
with st.spinner("Encrypting your PDF…"):
try:
# Write uploaded bytes to a temp input file
with tempfile.NamedTemporaryFile(
suffix=".pdf", delete=False
) as tmp_in:
tmp_in.write(uploaded_file.read())
tmp_in_path = Path(tmp_in.name)

# Encrypt to a temp output file
with tempfile.NamedTemporaryFile(
suffix=".pdf", delete=False
) as tmp_out:
tmp_out_path = Path(tmp_out.name)

encrypt_pdf(tmp_in_path, tmp_out_path, password)

# Read the encrypted bytes and offer download
encrypted_bytes = tmp_out_path.read_bytes()

st.success("✅ PDF encrypted successfully!")
st.download_button(
label="⬇️ Download encrypted PDF",
data=encrypted_bytes,
file_name=output_name,
mime="application/pdf",
use_container_width=True,
)

except Exception as exc:
st.error(f"❌ Encryption failed: {exc}")
finally:
# Clean up temp files
tmp_in_path.unlink(missing_ok=True)
tmp_out_path.unlink(missing_ok=True)
Comment on lines +77 to +110

Copilot AI Feb 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tmp_in_path / tmp_out_path are created inside the try, but the finally always calls tmp_in_path.unlink(...) / tmp_out_path.unlink(...). If an exception occurs before either variable is assigned (e.g., temp file creation fails), the finally will raise UnboundLocalError and mask the original error. Initialize these paths to None before the try and guard the cleanup accordingly.

Copilot uses AI. Check for mistakes.

# ── Footer ─────────────────────────────────────────────────────────────────

st.markdown("---")
st.caption(
"passifypdf — open source PDF encryption tool. "
"[View on GitHub](https://github.com/SUPAIDEAS/passifypdf)"
)
4 changes: 4 additions & 0 deletions requirements-webui.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Requirements for the optional Streamlit web UI (app/streamlit_app.py)
# Install with: pip install -r requirements-webui.txt
streamlit>=1.35.0
Comment thread
nirmalchandra marked this conversation as resolved.
passifypdf
3 changes: 3 additions & 0 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.