diff --git a/CHANGELOG.md b/CHANGELOG.md index 436727d..cc630e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,12 @@ and this project adheres to [Semantic Versioning][]. [keep a changelog]: https://keepachangelog.com/en/1.0.0/ [semantic versioning]: https://semver.org/spec/v2.0.0.html +## [Unreleased] + +### New Features + +- `dso watermark` now supports files in PDF format. With this change, quarto reports using the watermark feature can + be rendered to PDF, too. ## v0.8.2 diff --git a/pyproject.toml b/pyproject.toml index 4ae39ac..5800d07 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,7 @@ dependencies = [ "pillow", "platformdirs", "pre-commit", + "pypdf", "pyyaml", "questionary", "rich-click", diff --git a/src/dso/watermark.py b/src/dso/watermark.py index 4751208..578c46d 100644 --- a/src/dso/watermark.py +++ b/src/dso/watermark.py @@ -8,6 +8,7 @@ import rich_click as click from PIL import Image, ImageDraw, ImageFont +from pypdf import PdfReader, PdfWriter from svgutils import compose from dso import assets @@ -162,10 +163,23 @@ def apply_and_save(self, input_image: Path | str, output_image: Path | str): class PDFWatermarker(Watermarker): """Add watermarks to PDF images. The watermark overlay will be a pixel graphic embedded in the svg.""" - # e.g. https://www.geeksforgeeks.org/working-with-pdf-files-in-python/ + # Inspired by https://www.geeksforgeeks.org/working-with-pdf-files-in-python/ def apply_and_save(self, input_image: Path | str, output_image: Path | str): """Apply the watermark to an image and save it to the specified output file""" - raise NotImplementedError + reader = PdfReader(input_image) + writer = PdfWriter() + for page_obj in reader.pages: + size = (int(page_obj.mediabox.width), int(page_obj.mediabox.height)) + watermark_overlay = self.get_watermark_overlay(size) + with tempfile.NamedTemporaryFile(suffix=".pdf") as tf: + watermark_overlay.save(tf) + watermark_overlay_pdf = PdfReader(tf.file).pages[0] + page_obj.merge_page(watermark_overlay_pdf) + writer.add_page(page_obj) + + with open(output_image, "wb") as f: + writer.write(f) + reader.close() @click.command(name="watermark") diff --git a/tests/data/git_logo.pdf b/tests/data/git_logo.pdf new file mode 100644 index 0000000..d7ce077 Binary files /dev/null and b/tests/data/git_logo.pdf differ diff --git a/tests/data/lorem_ipsum.pdf b/tests/data/lorem_ipsum.pdf new file mode 100755 index 0000000..28eb6ff Binary files /dev/null and b/tests/data/lorem_ipsum.pdf differ diff --git a/tests/test_pandocfilter.py b/tests/test_pandocfilter.py index 60d2502..bfb7a87 100644 --- a/tests/test_pandocfilter.py +++ b/tests/test_pandocfilter.py @@ -7,6 +7,7 @@ def test_pandocfilter(quarto_stage): copyfile(TESTDATA / "git_logo.png", quarto_stage / "src" / "git_logo.png") + copyfile(TESTDATA / "git_logo.pdf", quarto_stage / "src" / "git_logo.pdf") copyfile(TESTDATA / "git_logo.svg", quarto_stage / "src" / "git_logo.svg") copyfile(TESTDATA / "git_logo.svg", quarto_stage / "src" / "git logo.svg") (quarto_stage / "src" / "_quarto.yml").write_text( @@ -40,6 +41,10 @@ def test_pandocfilter(quarto_stage): ![PNG Image](git_logo.png) + a PDF Image + + ![PDF Image](git_logo.pdf) + and an SVG image ![SVG Image](git_logo.svg) diff --git a/tests/test_watermark.py b/tests/test_watermark.py index fca07d8..8e24e5b 100644 --- a/tests/test_watermark.py +++ b/tests/test_watermark.py @@ -4,7 +4,7 @@ from click.testing import CliRunner from PIL import Image -from dso.watermark import SVGWatermarker, Watermarker, cli +from dso.watermark import PDFWatermarker, SVGWatermarker, Watermarker, cli from tests.conftest import TESTDATA @@ -30,6 +30,18 @@ def test_add_watermark_svg(tmp_path): wm.apply_and_save(TESTDATA / "git_logo.svg", tmp_path / "git_logo_watermarked.svg") +@pytest.mark.parametrize( + "pdf_file", + [ + "git_logo.pdf", # single page, pixel + "lorem_ipsum.pdf", # multi page, vector + ], +) +def test_add_watermark_pdf(tmp_path, pdf_file): + wm = PDFWatermarker("test") + wm.apply_and_save(TESTDATA / pdf_file, tmp_path / pdf_file) + + @pytest.mark.parametrize( "params", [