diff --git a/MANIFEST.in b/MANIFEST.in index 300ad2fa1..7de274470 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -42,6 +42,7 @@ recursive-include src *.py recursive-include src *.svg recursive-include src *.txt recursive-include src Makefile +recursive-include src *.pdf recursive-exclude doc * recursive-exclude deployment * diff --git a/pyproject.toml b/pyproject.toml index f06ee4b53..9f062bd19 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -129,6 +129,7 @@ dev = [ "pytest==9.1.*", "playwright", "responses", + "pypdfium2" ] [project.entry-points."distutils.commands"] diff --git a/src/tests/conftest.py b/src/tests/conftest.py index da5628856..2406c23e3 100644 --- a/src/tests/conftest.py +++ b/src/tests/conftest.py @@ -21,6 +21,8 @@ # import inspect import os +import re +from pathlib import Path import pytest from django.core.cache import cache @@ -28,7 +30,6 @@ from django.test import override_settings from django.utils import translation from django_scopes import scopes_disabled from fakeredis import FakeRedisConnection -from hierarkey.proxy import dirty_cache_keys from xdist.dsession import DSession from pretix.testutils.mock import get_redis_connection @@ -85,6 +86,7 @@ def reset_locale(): @pytest.fixture(autouse=True) def reset_hierarkey_cache_state(): + from hierarkey.proxy import dirty_cache_keys dirty_cache_keys.set(set()) @@ -143,3 +145,23 @@ def set_lock_namespaces(request): @pytest.fixture def class_monkeypatch(request, monkeypatch): request.cls.monkeypatch = monkeypatch + + +def pytest_addoption(parser): + parser.addoption( + '--pdf-dir', action='store', help="output dir for files generated by pdf tests", type=Path + ) + parser.addoption( + '--pdf-tmp-keep', action='store_true', help="keep the temporary directory created for pdf tests" + ) + + +@pytest.fixture +def pdf_dir(request, tmp_path_factory): + dirname = re.sub('[^a-zA-Z0-9]+', '_', request.node.name).strip("_") + if dir := request.config.getoption('--pdf-dir'): + pdf_dir: Path = (dir / dirname).absolute() + pdf_dir.mkdir(exist_ok=True, parents=True) + yield pdf_dir + else: + yield tmp_path_factory.mktemp(dirname) diff --git a/src/tests/plugins/badges/assets/bg-cropbox/bg.pdf b/src/tests/plugins/badges/assets/bg-cropbox/bg.pdf new file mode 100644 index 000000000..a81d104ae --- /dev/null +++ b/src/tests/plugins/badges/assets/bg-cropbox/bg.pdf @@ -0,0 +1,99 @@ +%PDF-1.3 +%âãÏÓ +1 0 obj +<< +/PageMode /UseNone +/Pages 2 0 R +/Type /Catalog +>> +endobj +2 0 obj +<< +/Count 1 +/Kids [ 3 0 R ] +/Type /Pages +>> +endobj +3 0 obj +<< +/Contents 4 0 R +/MediaBox [ 0 0 595.2756 841.8898 ] +/Parent 2 0 R +/Resources << +/Font 5 0 R +/ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> +/Rotate 0 +/Trans << +>> +/Type /Page +/CropBox [ 10 10 307.84 220 ] +>> +endobj +4 0 obj +<< +/Length 117 +>> +stream +q +1 0.0 0.0 1 10 10 cm +1 0 0 1 0 0 cm +BT +/F1 12 Tf +14.4 TL +ET +1 0 0 rg +n +56.69291 56.69291 141.7323 28.34646 re +B* +Q + +endstream +endobj +5 0 obj +<< +/F1 6 0 R +>> +endobj +6 0 obj +<< +/BaseFont /Helvetica +/Encoding /WinAnsiEncoding +/Name /F1 +/Subtype /Type1 +/Type /Font +>> +endobj +7 0 obj +<< +/Author (anonymous) +/CreationDate (D\07220260917173038\05302\04700\047) +/Creator (anonymous) +/Keywords () +/ModDate (D\07220260917173038\05302\04700\047) +/Producer (ReportLab PDF Library \055 \050opensource\051) +/Subject (unspecified) +/Title (untitled) +/Trapped /False +>> +endobj +xref +0 8 +0000000000 65535 f +0000000015 00000 n +0000000083 00000 n +0000000142 00000 n +0000000371 00000 n +0000000539 00000 n +0000000570 00000 n +0000000677 00000 n +trailer +<< +/Size 8 +/Root 1 0 R +/Info 7 0 R +/ID [ ] +>> +startxref +966 +%%EOF diff --git a/src/tests/plugins/badges/assets/bg-cropbox/expected.pdf b/src/tests/plugins/badges/assets/bg-cropbox/expected.pdf new file mode 100644 index 000000000..e0f7a11e4 --- /dev/null +++ b/src/tests/plugins/badges/assets/bg-cropbox/expected.pdf @@ -0,0 +1,213 @@ +%PDF-1.3 +%âãÏÓ +1 0 obj +<< +/Producer (pypdf) +>> +endobj +2 0 obj +<< +/Type /Pages +/Count 2 +/Kids [ 4 0 R 11 0 R ] +>> +endobj +3 0 obj +<< +/Type /Catalog +/Pages 2 0 R +>> +endobj +4 0 obj +<< +/Contents 5 0 R +/MediaBox [ 0 0 297.84 210 ] +/Resources << +/Font 6 0 R +/ProcSet [ /ImageB /ImageC /ImageI /PDF /Text ] +>> +/Rotate 0 +/Trans << +>> +/Type /Page +/Parent 2 0 R +>> +endobj +5 0 obj +[ 9 0 R 10 0 R ] +endobj +6 0 obj +<< +/F1 7 0 R +/F1-0 8 0 R +>> +endobj +7 0 obj +<< +/BaseFont /Helvetica +/Encoding /WinAnsiEncoding +/Name /F1 +/Subtype /Type1 +/Type /Font +>> +endobj +8 0 obj +<< +/BaseFont /Helvetica +/Encoding /WinAnsiEncoding +/Name /F1 +/Subtype /Type1 +/Type /Font +>> +endobj +9 0 obj +<< +/Length 170 +>> +stream +q +1 0.0 0.0 1 -10 -10 cm +10 10 297.84 210 re +W +n +q +1 0.0 0.0 1 10 10 cm +1 0 0 1 0 0 cm +BT +/F1-0 12 Tf +14.4 TL +ET +1 0 0 rg +n +56.69291 56.69291 141.7323 28.34646 re +B* +Q +Q + +endstream +endobj +10 0 obj +<< +/Length 106 +>> +stream +q +1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET +q +1 0 0 1 37.10551 140.9669 cm +q +1 0 0 1 0 5.669531 cm +Q +Q + + +Q + +endstream +endobj +11 0 obj +<< +/Contents 12 0 R +/MediaBox [ 0 0 297.84 210 ] +/Resources << +/Font 13 0 R +/ProcSet [ /ImageB /ImageC /ImageI /PDF /Text ] +>> +/Rotate 0 +/Trans << +>> +/Type /Page +/Parent 2 0 R +>> +endobj +12 0 obj +[ 15 0 R 16 0 R ] +endobj +13 0 obj +<< +/F1 14 0 R +/F1-0 8 0 R +>> +endobj +14 0 obj +<< +/BaseFont /Helvetica +/Encoding /WinAnsiEncoding +/Name /F1 +/Subtype /Type1 +/Type /Font +>> +endobj +15 0 obj +<< +/Length 170 +>> +stream +q +1 0.0 0.0 1 -10 -10 cm +10 10 297.84 210 re +W +n +q +1 0.0 0.0 1 10 10 cm +1 0 0 1 0 0 cm +BT +/F1-0 12 Tf +14.4 TL +ET +1 0 0 rg +n +56.69291 56.69291 141.7323 28.34646 re +B* +Q +Q + +endstream +endobj +16 0 obj +<< +/Length 106 +>> +stream +q +1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET +q +1 0 0 1 37.10551 140.9669 cm +q +1 0 0 1 0 5.669531 cm +Q +Q + + +Q + +endstream +endobj +xref +0 17 +0000000000 65535 f +0000000015 00000 n +0000000054 00000 n +0000000120 00000 n +0000000169 00000 n +0000000361 00000 n +0000000393 00000 n +0000000436 00000 n +0000000543 00000 n +0000000650 00000 n +0000000871 00000 n +0000001029 00000 n +0000001224 00000 n +0000001258 00000 n +0000001303 00000 n +0000001411 00000 n +0000001633 00000 n +trailer +<< +/Size 17 +/Root 3 0 R +/Info 1 0 R +>> +startxref +1791 +%%EOF diff --git a/src/tests/plugins/badges/assets/bg-mediabox-offset/bg.pdf b/src/tests/plugins/badges/assets/bg-mediabox-offset/bg.pdf new file mode 100644 index 000000000..85ac42c23 --- /dev/null +++ b/src/tests/plugins/badges/assets/bg-mediabox-offset/bg.pdf @@ -0,0 +1,98 @@ +%PDF-1.3 +%âãÏÓ +1 0 obj +<< +/PageMode /UseNone +/Pages 2 0 R +/Type /Catalog +>> +endobj +2 0 obj +<< +/Count 1 +/Kids [ 3 0 R ] +/Type /Pages +>> +endobj +3 0 obj +<< +/Contents 4 0 R +/MediaBox [ 10 10 307.84 220 ] +/Parent 2 0 R +/Resources << +/Font 5 0 R +/ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> +/Rotate 0 +/Trans << +>> +/Type /Page +>> +endobj +4 0 obj +<< +/Length 117 +>> +stream +q +1 0.0 0.0 1 10 10 cm +1 0 0 1 0 0 cm +BT +/F1 12 Tf +14.4 TL +ET +1 0 0 rg +n +56.69291 56.69291 141.7323 28.34646 re +B* +Q + +endstream +endobj +5 0 obj +<< +/F1 6 0 R +>> +endobj +6 0 obj +<< +/BaseFont /Helvetica +/Encoding /WinAnsiEncoding +/Name /F1 +/Subtype /Type1 +/Type /Font +>> +endobj +7 0 obj +<< +/Author (anonymous) +/CreationDate (D\07220260917173038\05302\04700\047) +/Creator (anonymous) +/Keywords () +/ModDate (D\07220260917173038\05302\04700\047) +/Producer (ReportLab PDF Library \055 \050opensource\051) +/Subject (unspecified) +/Title (untitled) +/Trapped /False +>> +endobj +xref +0 8 +0000000000 65535 f +0000000015 00000 n +0000000083 00000 n +0000000142 00000 n +0000000336 00000 n +0000000504 00000 n +0000000535 00000 n +0000000642 00000 n +trailer +<< +/Size 8 +/Root 1 0 R +/Info 7 0 R +/ID [ ] +>> +startxref +931 +%%EOF diff --git a/src/tests/plugins/badges/assets/bg-mediabox-offset/expected.pdf b/src/tests/plugins/badges/assets/bg-mediabox-offset/expected.pdf new file mode 100644 index 000000000..e0f7a11e4 --- /dev/null +++ b/src/tests/plugins/badges/assets/bg-mediabox-offset/expected.pdf @@ -0,0 +1,213 @@ +%PDF-1.3 +%âãÏÓ +1 0 obj +<< +/Producer (pypdf) +>> +endobj +2 0 obj +<< +/Type /Pages +/Count 2 +/Kids [ 4 0 R 11 0 R ] +>> +endobj +3 0 obj +<< +/Type /Catalog +/Pages 2 0 R +>> +endobj +4 0 obj +<< +/Contents 5 0 R +/MediaBox [ 0 0 297.84 210 ] +/Resources << +/Font 6 0 R +/ProcSet [ /ImageB /ImageC /ImageI /PDF /Text ] +>> +/Rotate 0 +/Trans << +>> +/Type /Page +/Parent 2 0 R +>> +endobj +5 0 obj +[ 9 0 R 10 0 R ] +endobj +6 0 obj +<< +/F1 7 0 R +/F1-0 8 0 R +>> +endobj +7 0 obj +<< +/BaseFont /Helvetica +/Encoding /WinAnsiEncoding +/Name /F1 +/Subtype /Type1 +/Type /Font +>> +endobj +8 0 obj +<< +/BaseFont /Helvetica +/Encoding /WinAnsiEncoding +/Name /F1 +/Subtype /Type1 +/Type /Font +>> +endobj +9 0 obj +<< +/Length 170 +>> +stream +q +1 0.0 0.0 1 -10 -10 cm +10 10 297.84 210 re +W +n +q +1 0.0 0.0 1 10 10 cm +1 0 0 1 0 0 cm +BT +/F1-0 12 Tf +14.4 TL +ET +1 0 0 rg +n +56.69291 56.69291 141.7323 28.34646 re +B* +Q +Q + +endstream +endobj +10 0 obj +<< +/Length 106 +>> +stream +q +1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET +q +1 0 0 1 37.10551 140.9669 cm +q +1 0 0 1 0 5.669531 cm +Q +Q + + +Q + +endstream +endobj +11 0 obj +<< +/Contents 12 0 R +/MediaBox [ 0 0 297.84 210 ] +/Resources << +/Font 13 0 R +/ProcSet [ /ImageB /ImageC /ImageI /PDF /Text ] +>> +/Rotate 0 +/Trans << +>> +/Type /Page +/Parent 2 0 R +>> +endobj +12 0 obj +[ 15 0 R 16 0 R ] +endobj +13 0 obj +<< +/F1 14 0 R +/F1-0 8 0 R +>> +endobj +14 0 obj +<< +/BaseFont /Helvetica +/Encoding /WinAnsiEncoding +/Name /F1 +/Subtype /Type1 +/Type /Font +>> +endobj +15 0 obj +<< +/Length 170 +>> +stream +q +1 0.0 0.0 1 -10 -10 cm +10 10 297.84 210 re +W +n +q +1 0.0 0.0 1 10 10 cm +1 0 0 1 0 0 cm +BT +/F1-0 12 Tf +14.4 TL +ET +1 0 0 rg +n +56.69291 56.69291 141.7323 28.34646 re +B* +Q +Q + +endstream +endobj +16 0 obj +<< +/Length 106 +>> +stream +q +1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET +q +1 0 0 1 37.10551 140.9669 cm +q +1 0 0 1 0 5.669531 cm +Q +Q + + +Q + +endstream +endobj +xref +0 17 +0000000000 65535 f +0000000015 00000 n +0000000054 00000 n +0000000120 00000 n +0000000169 00000 n +0000000361 00000 n +0000000393 00000 n +0000000436 00000 n +0000000543 00000 n +0000000650 00000 n +0000000871 00000 n +0000001029 00000 n +0000001224 00000 n +0000001258 00000 n +0000001303 00000 n +0000001411 00000 n +0000001633 00000 n +trailer +<< +/Size 17 +/Root 3 0 R +/Info 1 0 R +>> +startxref +1791 +%%EOF diff --git a/src/tests/plugins/badges/assets/bg-rotated/bg.pdf b/src/tests/plugins/badges/assets/bg-rotated/bg.pdf new file mode 100644 index 000000000..6be972dea Binary files /dev/null and b/src/tests/plugins/badges/assets/bg-rotated/bg.pdf differ diff --git a/src/tests/plugins/badges/assets/bg-rotated/expected.pdf b/src/tests/plugins/badges/assets/bg-rotated/expected.pdf new file mode 100644 index 000000000..0e8ee99c9 --- /dev/null +++ b/src/tests/plugins/badges/assets/bg-rotated/expected.pdf @@ -0,0 +1,216 @@ +%PDF-1.7 +%âãÏÓ +1 0 obj +<< +/Producer (pypdf) +>> +endobj +2 0 obj +<< +/Type /Pages +/Count 2 +/Kids [ 4 0 R 11 0 R ] +>> +endobj +3 0 obj +<< +/Type /Catalog +/Pages 2 0 R +>> +endobj +4 0 obj +<< +/Contents 5 0 R +/MediaBox [ 0 0 210 297.84 ] +/Resources << +/Font 6 0 R +/ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +/ExtGState << +/GS0 8 0 R +>> +>> +/Rotate 0 +/Trans << +>> +/Type /Page +/Parent 2 0 R +>> +endobj +5 0 obj +[ 9 0 R 10 0 R ] +endobj +6 0 obj +<< +/F1 7 0 R +>> +endobj +7 0 obj +<< +/BaseFont /Helvetica +/Encoding /WinAnsiEncoding +/Name /F1 +/Subtype /Type1 +/Type /Font +>> +endobj +8 0 obj +<< +/Type /ExtGState +/OP false +/op false +/OPM 1 +>> +endobj +9 0 obj +<< +/Length 287 +>> +stream +q +0.00000000000000006123234 -1 1 0.00000000000000006123234 0.0 297.84 cm +0.0 0.0 297.84 210 re +W +n +/GS0 gs +0.925811 0.881331 0.538643 rg +0 0.23622 297.638 209.764 re +f* +0.476631 0.756542 0.260899 rg +0 0.23622 297.638 209.764 re +292.638 205 m +5 205 l +5 5.23622 l +292.638 5.23622 l +h +f* +Q + +endstream +endobj +10 0 obj +<< +/Length 106 +>> +stream +q +1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET +q +1 0 0 1 37.10551 140.9669 cm +q +1 0 0 1 0 5.669531 cm +Q +Q + + +Q + +endstream +endobj +11 0 obj +<< +/Contents 12 0 R +/MediaBox [ 0 0 210 297.84 ] +/Resources << +/Font 13 0 R +/ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +/ExtGState << +/GS0 8 0 R +>> +>> +/Rotate 0 +/Trans << +>> +/Type /Page +/Parent 2 0 R +>> +endobj +12 0 obj +[ 15 0 R 16 0 R ] +endobj +13 0 obj +<< +/F1 14 0 R +>> +endobj +14 0 obj +<< +/BaseFont /Helvetica +/Encoding /WinAnsiEncoding +/Name /F1 +/Subtype /Type1 +/Type /Font +>> +endobj +15 0 obj +<< +/Length 287 +>> +stream +q +0.00000000000000006123234 -1 1 0.00000000000000006123234 0.0 297.84 cm +0.0 0.0 297.84 210 re +W +n +/GS0 gs +0.925811 0.881331 0.538643 rg +0 0.23622 297.638 209.764 re +f* +0.476631 0.756542 0.260899 rg +0 0.23622 297.638 209.764 re +292.638 205 m +5 205 l +5 5.23622 l +292.638 5.23622 l +h +f* +Q + +endstream +endobj +16 0 obj +<< +/Length 106 +>> +stream +q +1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET +q +1 0 0 1 37.10551 140.9669 cm +q +1 0 0 1 0 5.669531 cm +Q +Q + + +Q + +endstream +endobj +xref +0 17 +0000000000 65535 f +0000000015 00000 n +0000000054 00000 n +0000000120 00000 n +0000000169 00000 n +0000000389 00000 n +0000000421 00000 n +0000000452 00000 n +0000000559 00000 n +0000000624 00000 n +0000000962 00000 n +0000001120 00000 n +0000001343 00000 n +0000001377 00000 n +0000001410 00000 n +0000001518 00000 n +0000001857 00000 n +trailer +<< +/Size 17 +/Root 3 0 R +/Info 1 0 R +>> +startxref +2015 +%%EOF diff --git a/src/tests/plugins/badges/test_pdf.py b/src/tests/plugins/badges/test_pdf.py index 8dcdc8554..c8b9b696a 100644 --- a/src/tests/plugins/badges/test_pdf.py +++ b/src/tests/plugins/badges/test_pdf.py @@ -32,13 +32,18 @@ # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations under the License. +import logging from datetime import timedelta from decimal import Decimal from io import BytesIO +from pathlib import Path +import pypdfium2 import pytest +from django.core.files import File from django.utils.timezone import now from django_scopes import scope +from PIL import ImageChops from pypdf import PdfReader from pretix.base.models import ( @@ -118,3 +123,53 @@ def test_generate_pdf_multi(env): assert ftype == 'application/pdf' pdf = PdfReader(BytesIO(buf)) assert len(pdf.pages) == 1 + + +def asset_path(name): + return Path(__file__).parent / "assets" / name + + +def compare_pdfs(pdf_dir: Path, inp_a: Path | bytes, inp_b: Path | bytes): + logging.info(f"Comparing pdfs, writing files to {pdf_dir}") + + pdf_a = pypdfium2.PdfDocument(inp_a) + pdf_b = pypdfium2.PdfDocument(inp_b) + + pdf_a.save(pdf_dir / "a.pdf") + pdf_b.save(pdf_dir / "b.pdf") + + assert len(pdf_a) == len(pdf_b) + + for i, (page_a, page_b) in enumerate(zip(pdf_a, pdf_b)): + render_a = page_a.render() + render_b = page_b.render() + assert render_a.height == render_b.height + assert render_a.width == render_b.width + + diff = ImageChops.difference(render_a.to_pil(), render_b.to_pil()) + if diff.getbbox(): + diff.save(pdf_dir / f"{i}.png") + assert not diff.getbbox(), f"Page {i} differs." + + +@pytest.mark.django_db +@pytest.mark.parametrize("case", [ + "bg-rotated", + "bg-mediabox-offset", + "bg-cropbox" +]) +def test_generate_pdf_weird_bgs(pdf_dir, env, case): + event, order, shirt = env + asset_folder = asset_path(case) + with open(asset_folder / "bg.pdf", 'rb') as fi: + event.badge_layouts.create(name="Default", default=True, background=File(fi, name="test.pdf")) + e = BadgeExporter(event, organizer=event.organizer) + fname, ftype, buf = e.render({ + 'items': [shirt.pk], + 'rendering': 'one', + 'include_pending': True + }) + assert ftype == 'application/pdf' + assert buf + + compare_pdfs(pdf_dir, buf, asset_folder / "expected.pdf")