diff --git a/AGENTS.md b/AGENTS.md index 17853711..844ec67d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,6 +16,7 @@ Read this file before editing. Use the [README documentation map](README.md#docu - Use one indent level for continuation lines rather than aligning beneath an earlier expression. - Explain undocumented CEWE conventions, empirical calibration, ownership, and non-obvious Python mechanisms in focused comments. - Use descriptive domain names; avoid unrelated formatting, consistency renames, and architectural rewrites of working code. +- Write for contributors who may be new to Python or this project, including coding assistants. Prefer straightforward code and descriptive names over clever or cryptic abstractions. - Follow the repository's flake8/pylint configuration. Use narrow, explained suppressions rather than distorting a design to satisfy a metric. ## Platforms and file integrity diff --git a/requirements-pinned.txt b/requirements-pinned.txt index 7109b3e2..53ec7a4d 100644 --- a/requirements-pinned.txt +++ b/requirements-pinned.txt @@ -56,7 +56,7 @@ packaging==24.2 # pytest pikepdf==10.11.0 # via -r requirements.txt -pillow==12.3.0 +pillow==13.0.0 # via # -r requirements.txt # cairosvg diff --git a/requirements-winexe.txt b/requirements-winexe.txt index 9fe4db14..71ddc8ab 100644 --- a/requirements-winexe.txt +++ b/requirements-winexe.txt @@ -8,4 +8,4 @@ pefile==2024.8.26 pyinstaller==6.22.0 pyinstaller-hooks-contrib==2026.6 pywin32-ctypes==0.2.3 -setuptools==84.0.0 +setuptools==84.1.0 diff --git a/requirements.txt b/requirements.txt index b5a3a6fb..cb694459 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ cairosvg>=2.9.1 fonttools>=4.25.1 lxml>=4.4.1 packaging>=24.0,<25 -pillow>=5.4.1 +pillow>=13.0.0 # Required to render HEIC/HEIF photographs stored directly in an album. pillow-heif>=1.0 # Page.get_images(), used by the clipart test, was added in pikepdf 10.9. diff --git a/tests/testShadows/previous_result_pdfs/test_shadows.mcf.20261024S.pdf b/tests/testShadows/previous_result_pdfs/test_shadows.mcf.20261024S.pdf new file mode 100644 index 00000000..147478e1 Binary files /dev/null and b/tests/testShadows/previous_result_pdfs/test_shadows.mcf.20261024S.pdf differ diff --git a/tests/testTextLinks/test_links.py b/tests/testTextLinks/test_links.py index 44f43005..09b213be 100644 --- a/tests/testTextLinks/test_links.py +++ b/tests/testTextLinks/test_links.py @@ -3,8 +3,10 @@ from pathlib import Path import sys +from lxml import etree import pymupdf from reportlab.lib.styles import getSampleStyleSheet +from reportlab.pdfbase import pdfmetrics from reportlab.pdfgen import canvas from reportlab.platypus import Frame @@ -16,7 +18,7 @@ configureTestImportPaths(__file__) -from texts.links import _urlsInText, addWebLinksToPdf +from texts.links import _urlsInText, addWebLinksToPdf, getAlbumTextUrls from texts.markup import AddAlbumText from texts.outlines import TextEffectsParagraph @@ -60,6 +62,38 @@ def test_wrappedHttpUrlCreatesOneExternalPdfLink(tmp_path): assert {link['uri'] for link in links} == {url} +def test_urlFragmentsAcrossPdfTextRunsBecomeExternalLink(tmp_path): + outputFile = tmp_path / 'split-web-link.pdf' + url = 'https://storysontheroad2023.blogspot.com/' + pdf = canvas.Canvas(str(outputFile)) + font = 'Helvetica' + fontSize = 14 + xPosition = 40 + fragments = ( + ('https:/', fontSize), + (' ', 0.01), + ('/', fontSize), + (' ', 0.01), + ('storysontheroad2023.blogspot.com/', fontSize), + (' ', 0.01), + ) + for text, textSize in fragments: + pdf.setFont(font, textSize) + pdf.drawString(xPosition, 650, text) + xPosition += pdfmetrics.stringWidth(text, font, textSize) + pdf.save() + + addWebLinksToPdf(outputFile, {url}) + + with pymupdf.open(outputFile) as renderedPdf: + extractedText = renderedPdf[0].get_text() + links = renderedPdf[0].get_links() + + assert 'https:/ / storysontheroad2023.blogspot.com/' in extractedText + assert len(links) == 1 + assert links[0]['uri'] == url + + def test_albumTextEscapesUrlAndAddsInvisibleSlashBreaks(): assert AddAlbumText('', 'See https://example.com/a?x=1&y=2, please.') == ( 'See https:/ / ' @@ -73,3 +107,13 @@ def test_albumTextEscapesOrdinaryTextWithoutSubstitution(): def test_urlInParenthesesExcludesTheClosingParenthesis(): assert _urlsInText('(https://www.youtube.com/watch?v=zvB0jZKRKrk)') == { 'https://www.youtube.com/watch?v=zvB0jZKRKrk'} + + +def test_urlAtParagraphEndDoesNotAbsorbNextParagraphStart(): + album = etree.fromstring( + '' + 'Visit https://example.com/' + 'A second paragraph' + '
Visit https://example.com/
A second paragraph