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
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,12 @@ 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.
- Some PDF object warnings may appear for malformed source PDFs, depending on the input file.
- Emoji and other special characters in input/output file paths are supported.

## 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.

## Build & Run Locally
Visit [BUILD.md](BUILD.md)
Visit [BUILD.md](BUILD.md)
28 changes: 24 additions & 4 deletions passifypdf/encryptpdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import sys
from pathlib import Path
from typing import Union
from typing import TextIO, Union

from pypdf import PdfReader, PdfWriter

Expand Down Expand Up @@ -49,6 +49,23 @@ def encrypt_pdf(input_pdf: Union[str, Path], output_pdf: Union[str, Path], passw
raise Exception(f"Failed to encrypt PDF: {e}")


def _stream_safe_text(text: str, stream: TextIO) -> str:
"""Return text that can always be encoded by the target stream."""
encoding = getattr(stream, "encoding", None) or "utf-8"
try:
text.encode(encoding)
return text
except UnicodeEncodeError:
return text.encode(encoding, errors="backslashreplace").decode(encoding, errors="ignore")
except LookupError:
return text.encode("utf-8", errors="backslashreplace").decode("utf-8", errors="ignore")


def _safe_print(text: str, stream: TextIO) -> None:
"""Print text without failing on terminal encoding limitations."""
print(_stream_safe_text(text, stream), file=stream)


def main() -> int:
"""
Main function to run the CLI.
Expand All @@ -61,12 +78,15 @@ def main() -> int:

try:
encrypt_pdf(args.input, args.output, args.passwd)
print(f"Congratulations!\nPDF file encrypted successfully and saved as '{args.output}'")
_safe_print(
f"Congratulations!\nPDF file encrypted successfully and saved as '{args.output}'",
sys.stdout,
)
return 0
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
_safe_print(f"Error: {e}", sys.stderr)
return 1


if __name__ == "__main__":
sys.exit(main())
sys.exit(main())
38 changes: 38 additions & 0 deletions tests/integrationtests/test_encryptpdf_integration.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import os
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
from unittest import TestCase

from pypdf import PdfReader, PdfWriter
Expand Down Expand Up @@ -39,3 +44,36 @@ def test_encrypt_pdf_integration(self):
# Verify we can decrypt it
self.assertTrue(reader.decrypt(self.password))
self.assertEqual(len(reader.pages), 1)

def test_cli_ascii_stdout_handles_emoji_paths(self):
sample_pdf = Path(__file__).parent.parent / "resources" / "Sample_PDF.pdf"
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
input_pdf = temp_path / "in📄put😀.pdf"
output_pdf = temp_path / "out🔒file😀.pdf"
shutil.copy(sample_pdf, input_pdf)

env = os.environ.copy()
env["PYTHONIOENCODING"] = "ascii"

result = subprocess.run(
[
sys.executable,
"-m",
"passifypdf.encryptpdf",
"-i",
str(input_pdf),
"-o",
str(output_pdf),
"-p",
"secret123",
],
capture_output=True,
text=True,
env=env,
check=False,
)

self.assertEqual(result.returncode, 0, msg=result.stderr)
self.assertTrue(output_pdf.exists())
self.assertIn("\\U0001f512", result.stdout)
33 changes: 31 additions & 2 deletions tests/unittests/test_encryptpdf.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import argparse
import io
from unittest import TestCase
from unittest.mock import patch, mock_open
from passifypdf.encryptpdf import encrypt_pdf
from unittest.mock import MagicMock, mock_open, patch

from passifypdf.encryptpdf import encrypt_pdf, main


class TestPdfUnitTests(TestCase):
Expand Down Expand Up @@ -59,3 +62,29 @@ def test_encrypt_pdf_is_directory(self, mock_path_cls):

with self.assertRaises(IsADirectoryError):
encrypt_pdf("directory", "output.pdf", "secret")

@patch("passifypdf.encryptpdf.encrypt_pdf")
@patch("passifypdf.encryptpdf.get_arg_parser")
def test_main_succeeds_with_ascii_stdout_and_emoji_output_path(
self, mock_get_arg_parser, mock_encrypt_pdf
):
mock_parser = MagicMock()
mock_parser.parse_args.return_value = argparse.Namespace(
input="input.pdf",
output="out🔒file😀.pdf",
passwd="secret123",
)
mock_get_arg_parser.return_value = mock_parser

stdout_bytes = io.BytesIO()
stderr_bytes = io.BytesIO()
ascii_stdout = io.TextIOWrapper(stdout_bytes, encoding="ascii", errors="strict")
ascii_stderr = io.TextIOWrapper(stderr_bytes, encoding="ascii", errors="strict")

with patch("sys.stdout", ascii_stdout), patch("sys.stderr", ascii_stderr):
exit_code = main()
ascii_stdout.flush()

self.assertEqual(exit_code, 0)
self.assertIn("\\U0001f512", stdout_bytes.getvalue().decode("ascii"))
mock_encrypt_pdf.assert_called_once_with("input.pdf", "out🔒file😀.pdf", "secret123")
Loading