From 023dc4507505b1c865e01802ab85a6df91baf393 Mon Sep 17 00:00:00 2001 From: Julfried Date: Fri, 7 Aug 2026 14:01:43 +0200 Subject: [PATCH 01/12] Add checkbox appearance streams --- commonforms/form_creator.py | 42 ++++++++++++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/commonforms/form_creator.py b/commonforms/form_creator.py index 2a78a12..7416311 100644 --- a/commonforms/form_creator.py +++ b/commonforms/form_creator.py @@ -1,11 +1,12 @@ -from pypdf import PdfWriter, PdfReader +from pypdf import PdfReader, PdfWriter from pypdf.annotations import AnnotationDictionary from pypdf.generic import ( - NameObject, ArrayObject, + DecodedStreamObject, + DictionaryObject, + NameObject, NumberObject, TextStringObject, - DictionaryObject, ) from commonforms.utils import BoundingBox @@ -83,6 +84,38 @@ def __init__( super().__init__() pdf_value = NameObject("/Off") if not value else NameObject("/Yes") + width = max(float(rect[2]) - float(rect[0]), 1) + height = max(float(rect[3]) - float(rect[1]), 1) + + def appearance(checked: bool) -> DecodedStreamObject: + stream = DecodedStreamObject() + stream.update( + { + NameObject("/Type"): NameObject("/XObject"), + NameObject("/Subtype"): NameObject("/Form"), + NameObject("/BBox"): ArrayObject( + [NumberObject(0), NumberObject(0), NumberObject(width), NumberObject(height)] + ), + } + ) + if checked: + stream.set_data( + f"q 0 0 0 RG {min(width, height) * 0.12:.4f} w " + f"{width * 0.18:.4f} {height * 0.50:.4f} m " + f"{width * 0.43:.4f} {height * 0.23:.4f} l " + f"{width * 0.84:.4f} {height * 0.78:.4f} l S Q".encode() + ) + else: + stream.set_data(b"") + return stream + + normal_appearance = DictionaryObject( + { + NameObject("/Off"): appearance(False), + NameObject("/Yes"): appearance(True), + } + ) + self.update( { NameObject("/Type"): NameObject("/Annot"), @@ -93,6 +126,9 @@ def __init__( NameObject("/Rect"): rect, NameObject("/V"): pdf_value, NameObject("/AS"): pdf_value, + NameObject("/AP"): DictionaryObject( + {NameObject("/N"): normal_appearance} + ), NameObject("/T"): TextStringObject(name), } ) From 091708d536aed43d316dead551e29aa87189cfc6 Mon Sep 17 00:00:00 2001 From: Julfried Date: Fri, 7 Aug 2026 14:10:37 +0200 Subject: [PATCH 02/12] Store checkbox appearances indirectly --- commonforms/form_creator.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/commonforms/form_creator.py b/commonforms/form_creator.py index 7416311..44894dd 100644 --- a/commonforms/form_creator.py +++ b/commonforms/form_creator.py @@ -198,6 +198,10 @@ def add_text_box( def add_checkbox(self, name: str, page: int, bounding_box: BoundingBox) -> None: rect = rect_for(bounding_box, self.writer.pages[page]) checkbox = Checkbox(name=name, rect=rect) + appearance = checkbox[NameObject("/AP")]) + normal_appearance = appearance[NameObject("/N")]) + for state in (NameObject("/Off"), NameObject("/Yes")): + normal_appearance[state] = self.writer._add_object(normal_appearance[state]) self.writer.add_annotation(page_number=page, annotation=checkbox) def add_signature(self, name: str, page: int, bounding_box: BoundingBox) -> None: From df558fee187bf2cb69f1bd2873db25a390e6426b Mon Sep 17 00:00:00 2001 From: Julfried Date: Fri, 7 Aug 2026 14:19:13 +0200 Subject: [PATCH 03/12] Add checkbox appearance resources --- commonforms/form_creator.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/commonforms/form_creator.py b/commonforms/form_creator.py index 44894dd..010d6ac 100644 --- a/commonforms/form_creator.py +++ b/commonforms/form_creator.py @@ -96,6 +96,7 @@ def appearance(checked: bool) -> DecodedStreamObject: NameObject("/BBox"): ArrayObject( [NumberObject(0), NumberObject(0), NumberObject(width), NumberObject(height)] ), + NameObject("/Resources"): DictionaryObject(), } ) if checked: @@ -198,8 +199,8 @@ def add_text_box( def add_checkbox(self, name: str, page: int, bounding_box: BoundingBox) -> None: rect = rect_for(bounding_box, self.writer.pages[page]) checkbox = Checkbox(name=name, rect=rect) - appearance = checkbox[NameObject("/AP")]) - normal_appearance = appearance[NameObject("/N")]) + appearance = checkbox[NameObject("/AP")] + normal_appearance = appearance[NameObject("/N")] for state in (NameObject("/Off"), NameObject("/Yes")): normal_appearance[state] = self.writer._add_object(normal_appearance[state]) self.writer.add_annotation(page_number=page, annotation=checkbox) From 4eed4c5b0721cd2d3ddd84cc6581f4c85a1b8876 Mon Sep 17 00:00:00 2001 From: Julfried Date: Fri, 7 Aug 2026 16:32:55 +0200 Subject: [PATCH 04/12] Draw checkbox appearance borders --- commonforms/form_creator.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/commonforms/form_creator.py b/commonforms/form_creator.py index 010d6ac..b05e133 100644 --- a/commonforms/form_creator.py +++ b/commonforms/form_creator.py @@ -88,6 +88,13 @@ def __init__( height = max(float(rect[3]) - float(rect[1]), 1) def appearance(checked: bool) -> DecodedStreamObject: + border_width = min(width, height) * 0.08 + border_inset = border_width + border = ( + f"q 0 0 0 RG {border_width:.4f} w " + f"{border_inset:.4f} {border_inset:.4f} " + f"{width - 2 * border_inset:.4f} {height - 2 * border_inset:.4f} re S Q" + ) stream = DecodedStreamObject() stream.update( { @@ -101,13 +108,13 @@ def appearance(checked: bool) -> DecodedStreamObject: ) if checked: stream.set_data( - f"q 0 0 0 RG {min(width, height) * 0.12:.4f} w " + f"{border} q 0 0 0 RG {min(width, height) * 0.12:.4f} w " f"{width * 0.18:.4f} {height * 0.50:.4f} m " f"{width * 0.43:.4f} {height * 0.23:.4f} l " f"{width * 0.84:.4f} {height * 0.78:.4f} l S Q".encode() ) else: - stream.set_data(b"") + stream.set_data(border.encode()) return stream normal_appearance = DictionaryObject( From bc39664e73fa33f6b6f162619c421f9fcff0cf92 Mon Sep 17 00:00:00 2001 From: Julfried Date: Fri, 7 Aug 2026 17:54:15 +0200 Subject: [PATCH 05/12] Test checkbox appearance flattening --- tests/checkbox_appearance_test.py | 71 +++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 tests/checkbox_appearance_test.py diff --git a/tests/checkbox_appearance_test.py b/tests/checkbox_appearance_test.py new file mode 100644 index 0000000..46ed095 --- /dev/null +++ b/tests/checkbox_appearance_test.py @@ -0,0 +1,71 @@ +from pypdf import PdfReader, PdfWriter +from pypdf.generic import ContentStream, IndirectObject, NameObject + +from commonforms.form_creator import PyPdfFormCreator +from commonforms.utils import BoundingBox + + +def test_checkbox_appearance_streams_are_indirect_and_flatten_checked_state(tmp_path): + input_path = tmp_path / "blank.pdf" + checkbox_path = tmp_path / "checkbox.pdf" + flattened_path = tmp_path / "flattened.pdf" + + blank_writer = PdfWriter() + blank_writer.add_blank_page(width=612, height=792) + with input_path.open("wb") as output: + blank_writer.write(output) + blank_writer.close() + + creator = PyPdfFormCreator(input_path) + creator.add_checkbox( + "agree", 0, BoundingBox(x0=0.1, y0=0.1, x1=0.2, y1=0.2) + ) + creator.save(checkbox_path) + creator.close() + + checkbox_reader = PdfReader(checkbox_path) + annotation = checkbox_reader.pages[0]["/Annots"][0].get_object() + normal_appearances = annotation["/AP"]["/N"] + + for state in ("/Off", "/Yes"): + appearance_reference = normal_appearances.raw_get(NameObject(state)) + assert isinstance(appearance_reference, IndirectObject) + + appearance = appearance_reference.get_object() + assert appearance["/Type"] == "/XObject" + assert appearance["/Subtype"] == "/Form" + assert "/BBox" in appearance + assert "/Resources" in appearance + assert appearance.get_data() + + checked_appearance = normal_appearances["/Yes"] + checked_bbox = list(checked_appearance["/BBox"]) + checked_resources = dict(checked_appearance["/Resources"]) + checked_data = checked_appearance.get_data() + + flatten_writer = PdfWriter(clone_from=checkbox_reader) + flatten_writer.update_page_form_field_values( + None, {"agree": "/Yes"}, auto_regenerate=False, flatten=True + ) + with flattened_path.open("wb") as output: + flatten_writer.write(output) + flatten_writer.close() + checkbox_reader.close() + + flattened_reader = PdfReader(flattened_path) + flattened_page = flattened_reader.pages[0] + operations = ContentStream(flattened_page.get_contents(), flattened_reader).operations + do_operations = [operands for operands, operator in operations if operator == b"Do"] + + assert len(do_operations) == 1 + xobject_name = do_operations[0][0] + flattened_appearance = flattened_page["/Resources"]["/XObject"].raw_get( + xobject_name + ).get_object() + + assert flattened_appearance["/Type"] == "/XObject" + assert flattened_appearance["/Subtype"] == "/Form" + assert list(flattened_appearance["/BBox"]) == checked_bbox + assert dict(flattened_appearance["/Resources"]) == checked_resources + assert flattened_appearance.get_data() == checked_data + flattened_reader.close() From cffcef5f2e8c3cd27f54e99cb4f2132a918f944a Mon Sep 17 00:00:00 2001 From: Julfried Date: Fri, 7 Aug 2026 18:50:44 +0200 Subject: [PATCH 06/12] Validate checkbox appearance dictionaries --- commonforms/form_creator.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/commonforms/form_creator.py b/commonforms/form_creator.py index b05e133..fad108a 100644 --- a/commonforms/form_creator.py +++ b/commonforms/form_creator.py @@ -207,7 +207,15 @@ def add_checkbox(self, name: str, page: int, bounding_box: BoundingBox) -> None: rect = rect_for(bounding_box, self.writer.pages[page]) checkbox = Checkbox(name=name, rect=rect) appearance = checkbox[NameObject("/AP")] + if not isinstance(appearance, DictionaryObject): + raise TypeError( + "Generated checkbox has an invalid /AP appearance dictionary" + ) normal_appearance = appearance[NameObject("/N")] + if not isinstance(normal_appearance, DictionaryObject): + raise TypeError( + "Generated checkbox has an invalid /AP /N appearance dictionary" + ) for state in (NameObject("/Off"), NameObject("/Yes")): normal_appearance[state] = self.writer._add_object(normal_appearance[state]) self.writer.add_annotation(page_number=page, annotation=checkbox) From 45202a575383d1fce5a569d33e1e5fdfde1c1fff Mon Sep 17 00:00:00 2001 From: Julfried Date: Fri, 7 Aug 2026 18:51:44 +0200 Subject: [PATCH 07/12] Center checkbox checkmarks --- commonforms/form_creator.py | 14 ++++++--- tests/checkbox_appearance_test.py | 52 +++++++++++++++++++++++++++++-- 2 files changed, 60 insertions(+), 6 deletions(-) diff --git a/commonforms/form_creator.py b/commonforms/form_creator.py index fad108a..c066b01 100644 --- a/commonforms/form_creator.py +++ b/commonforms/form_creator.py @@ -107,11 +107,17 @@ def appearance(checked: bool) -> DecodedStreamObject: } ) if checked: + checkmark_size = min(width, height) + checkmark_x = (width - checkmark_size) / 2 + checkmark_y = (height - checkmark_size) / 2 stream.set_data( - f"{border} q 0 0 0 RG {min(width, height) * 0.12:.4f} w " - f"{width * 0.18:.4f} {height * 0.50:.4f} m " - f"{width * 0.43:.4f} {height * 0.23:.4f} l " - f"{width * 0.84:.4f} {height * 0.78:.4f} l S Q".encode() + f"{border} q 0 0 0 RG {checkmark_size * 0.12:.4f} w " + f"{checkmark_x + checkmark_size * 0.18:.4f} " + f"{checkmark_y + checkmark_size * 0.50:.4f} m " + f"{checkmark_x + checkmark_size * 0.43:.4f} " + f"{checkmark_y + checkmark_size * 0.23:.4f} l " + f"{checkmark_x + checkmark_size * 0.84:.4f} " + f"{checkmark_y + checkmark_size * 0.78:.4f} l S Q".encode() ) else: stream.set_data(border.encode()) diff --git a/tests/checkbox_appearance_test.py b/tests/checkbox_appearance_test.py index 46ed095..ac912ef 100644 --- a/tests/checkbox_appearance_test.py +++ b/tests/checkbox_appearance_test.py @@ -1,7 +1,16 @@ +import pytest from pypdf import PdfReader, PdfWriter -from pypdf.generic import ContentStream, IndirectObject, NameObject +from pypdf.generic import ( + ArrayObject, + ContentStream, + DecodedStreamObject, + DictionaryObject, + IndirectObject, + NameObject, + NumberObject, +) -from commonforms.form_creator import PyPdfFormCreator +from commonforms.form_creator import Checkbox, PyPdfFormCreator from commonforms.utils import BoundingBox @@ -69,3 +78,42 @@ def test_checkbox_appearance_streams_are_indirect_and_flatten_checked_state(tmp_ assert dict(flattened_appearance["/Resources"]) == checked_resources assert flattened_appearance.get_data() == checked_data flattened_reader.close() + + +@pytest.mark.parametrize(("width", "height"), [(100, 20), (20, 100)]) +def test_checkbox_checkmark_is_square_and_centered(width, height): + checkbox = Checkbox( + "agree", + ArrayObject( + [ + NumberObject(0), + NumberObject(0), + NumberObject(width), + NumberObject(height), + ] + ), + ) + appearance = checkbox["/AP"] + assert isinstance(appearance, DictionaryObject) + normal_appearances = appearance["/N"] + assert isinstance(normal_appearances, DictionaryObject) + checked_appearance = normal_appearances["/Yes"] + assert isinstance(checked_appearance, DecodedStreamObject) + operations = ContentStream(checked_appearance, None).operations + checkmark = [ + tuple(map(float, operands)) + for operands, operator in operations + if operator in (b"m", b"l") + ] + + size = min(width, height) + x = (width - size) / 2 + y = (height - size) / 2 + expected_checkmark = [ + (x + size * 0.18, y + size * 0.50), + (x + size * 0.43, y + size * 0.23), + (x + size * 0.84, y + size * 0.78), + ] + + for actual, expected in zip(checkmark, expected_checkmark, strict=True): + assert actual == pytest.approx(expected) From da74502082cc5ecbb23e53ef2237750252f18a31 Mon Sep 17 00:00:00 2001 From: Julfried Date: Fri, 7 Aug 2026 18:51:58 +0200 Subject: [PATCH 08/12] Apply ruff check --- commonforms/form_creator.py | 7 ++++++- tests/checkbox_appearance_test.py | 14 +++++++------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/commonforms/form_creator.py b/commonforms/form_creator.py index c066b01..88e19b1 100644 --- a/commonforms/form_creator.py +++ b/commonforms/form_creator.py @@ -101,7 +101,12 @@ def appearance(checked: bool) -> DecodedStreamObject: NameObject("/Type"): NameObject("/XObject"), NameObject("/Subtype"): NameObject("/Form"), NameObject("/BBox"): ArrayObject( - [NumberObject(0), NumberObject(0), NumberObject(width), NumberObject(height)] + [ + NumberObject(0), + NumberObject(0), + NumberObject(width), + NumberObject(height), + ] ), NameObject("/Resources"): DictionaryObject(), } diff --git a/tests/checkbox_appearance_test.py b/tests/checkbox_appearance_test.py index ac912ef..56324d8 100644 --- a/tests/checkbox_appearance_test.py +++ b/tests/checkbox_appearance_test.py @@ -26,9 +26,7 @@ def test_checkbox_appearance_streams_are_indirect_and_flatten_checked_state(tmp_ blank_writer.close() creator = PyPdfFormCreator(input_path) - creator.add_checkbox( - "agree", 0, BoundingBox(x0=0.1, y0=0.1, x1=0.2, y1=0.2) - ) + creator.add_checkbox("agree", 0, BoundingBox(x0=0.1, y0=0.1, x1=0.2, y1=0.2)) creator.save(checkbox_path) creator.close() @@ -63,14 +61,16 @@ def test_checkbox_appearance_streams_are_indirect_and_flatten_checked_state(tmp_ flattened_reader = PdfReader(flattened_path) flattened_page = flattened_reader.pages[0] - operations = ContentStream(flattened_page.get_contents(), flattened_reader).operations + operations = ContentStream( + flattened_page.get_contents(), flattened_reader + ).operations do_operations = [operands for operands, operator in operations if operator == b"Do"] assert len(do_operations) == 1 xobject_name = do_operations[0][0] - flattened_appearance = flattened_page["/Resources"]["/XObject"].raw_get( - xobject_name - ).get_object() + flattened_appearance = ( + flattened_page["/Resources"]["/XObject"].raw_get(xobject_name).get_object() + ) assert flattened_appearance["/Type"] == "/XObject" assert flattened_appearance["/Subtype"] == "/Form" From d8636cd871cd73f28cf0560d3e8fa13f837b6ce1 Mon Sep 17 00:00:00 2001 From: Julfried Date: Fri, 7 Aug 2026 19:27:06 +0200 Subject: [PATCH 09/12] Revert centered checkbox checkmarks --- commonforms/form_creator.py | 14 +++------ tests/checkbox_appearance_test.py | 52 ++----------------------------- 2 files changed, 6 insertions(+), 60 deletions(-) diff --git a/commonforms/form_creator.py b/commonforms/form_creator.py index 88e19b1..1f8cbb2 100644 --- a/commonforms/form_creator.py +++ b/commonforms/form_creator.py @@ -112,17 +112,11 @@ def appearance(checked: bool) -> DecodedStreamObject: } ) if checked: - checkmark_size = min(width, height) - checkmark_x = (width - checkmark_size) / 2 - checkmark_y = (height - checkmark_size) / 2 stream.set_data( - f"{border} q 0 0 0 RG {checkmark_size * 0.12:.4f} w " - f"{checkmark_x + checkmark_size * 0.18:.4f} " - f"{checkmark_y + checkmark_size * 0.50:.4f} m " - f"{checkmark_x + checkmark_size * 0.43:.4f} " - f"{checkmark_y + checkmark_size * 0.23:.4f} l " - f"{checkmark_x + checkmark_size * 0.84:.4f} " - f"{checkmark_y + checkmark_size * 0.78:.4f} l S Q".encode() + f"{border} q 0 0 0 RG {min(width, height) * 0.12:.4f} w " + f"{width * 0.18:.4f} {height * 0.50:.4f} m " + f"{width * 0.43:.4f} {height * 0.23:.4f} l " + f"{width * 0.84:.4f} {height * 0.78:.4f} l S Q".encode() ) else: stream.set_data(border.encode()) diff --git a/tests/checkbox_appearance_test.py b/tests/checkbox_appearance_test.py index 56324d8..4717fdd 100644 --- a/tests/checkbox_appearance_test.py +++ b/tests/checkbox_appearance_test.py @@ -1,16 +1,7 @@ -import pytest from pypdf import PdfReader, PdfWriter -from pypdf.generic import ( - ArrayObject, - ContentStream, - DecodedStreamObject, - DictionaryObject, - IndirectObject, - NameObject, - NumberObject, -) +from pypdf.generic import ContentStream, IndirectObject, NameObject -from commonforms.form_creator import Checkbox, PyPdfFormCreator +from commonforms.form_creator import PyPdfFormCreator from commonforms.utils import BoundingBox @@ -78,42 +69,3 @@ def test_checkbox_appearance_streams_are_indirect_and_flatten_checked_state(tmp_ assert dict(flattened_appearance["/Resources"]) == checked_resources assert flattened_appearance.get_data() == checked_data flattened_reader.close() - - -@pytest.mark.parametrize(("width", "height"), [(100, 20), (20, 100)]) -def test_checkbox_checkmark_is_square_and_centered(width, height): - checkbox = Checkbox( - "agree", - ArrayObject( - [ - NumberObject(0), - NumberObject(0), - NumberObject(width), - NumberObject(height), - ] - ), - ) - appearance = checkbox["/AP"] - assert isinstance(appearance, DictionaryObject) - normal_appearances = appearance["/N"] - assert isinstance(normal_appearances, DictionaryObject) - checked_appearance = normal_appearances["/Yes"] - assert isinstance(checked_appearance, DecodedStreamObject) - operations = ContentStream(checked_appearance, None).operations - checkmark = [ - tuple(map(float, operands)) - for operands, operator in operations - if operator in (b"m", b"l") - ] - - size = min(width, height) - x = (width - size) / 2 - y = (height - size) / 2 - expected_checkmark = [ - (x + size * 0.18, y + size * 0.50), - (x + size * 0.43, y + size * 0.23), - (x + size * 0.84, y + size * 0.78), - ] - - for actual, expected in zip(checkmark, expected_checkmark, strict=True): - assert actual == pytest.approx(expected) From e1018a8764aca278aa05e4c3747f4e7317ad4b8c Mon Sep 17 00:00:00 2001 From: Julfried Date: Sat, 8 Aug 2026 13:56:58 +0200 Subject: [PATCH 10/12] Revert checkbox appearance borders --- commonforms/form_creator.py | 11 ++--------- tests/checkbox_appearance_test.py | 3 ++- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/commonforms/form_creator.py b/commonforms/form_creator.py index 1f8cbb2..1533b22 100644 --- a/commonforms/form_creator.py +++ b/commonforms/form_creator.py @@ -88,13 +88,6 @@ def __init__( height = max(float(rect[3]) - float(rect[1]), 1) def appearance(checked: bool) -> DecodedStreamObject: - border_width = min(width, height) * 0.08 - border_inset = border_width - border = ( - f"q 0 0 0 RG {border_width:.4f} w " - f"{border_inset:.4f} {border_inset:.4f} " - f"{width - 2 * border_inset:.4f} {height - 2 * border_inset:.4f} re S Q" - ) stream = DecodedStreamObject() stream.update( { @@ -113,13 +106,13 @@ def appearance(checked: bool) -> DecodedStreamObject: ) if checked: stream.set_data( - f"{border} q 0 0 0 RG {min(width, height) * 0.12:.4f} w " + f"q 0 0 0 RG {min(width, height) * 0.12:.4f} w " f"{width * 0.18:.4f} {height * 0.50:.4f} m " f"{width * 0.43:.4f} {height * 0.23:.4f} l " f"{width * 0.84:.4f} {height * 0.78:.4f} l S Q".encode() ) else: - stream.set_data(border.encode()) + stream.set_data(b"") return stream normal_appearance = DictionaryObject( diff --git a/tests/checkbox_appearance_test.py b/tests/checkbox_appearance_test.py index 4717fdd..fd1436e 100644 --- a/tests/checkbox_appearance_test.py +++ b/tests/checkbox_appearance_test.py @@ -34,12 +34,13 @@ def test_checkbox_appearance_streams_are_indirect_and_flatten_checked_state(tmp_ assert appearance["/Subtype"] == "/Form" assert "/BBox" in appearance assert "/Resources" in appearance - assert appearance.get_data() + assert normal_appearances["/Off"].get_data() == b"" checked_appearance = normal_appearances["/Yes"] checked_bbox = list(checked_appearance["/BBox"]) checked_resources = dict(checked_appearance["/Resources"]) checked_data = checked_appearance.get_data() + assert checked_data flatten_writer = PdfWriter(clone_from=checkbox_reader) flatten_writer.update_page_form_field_values( From 8d2a0807b353da3719cdbcd705de8e7a41a9d796 Mon Sep 17 00:00:00 2001 From: Julfried Date: Sat, 8 Aug 2026 14:12:57 +0200 Subject: [PATCH 11/12] Allow whitespace in empty appearances --- tests/checkbox_appearance_test.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/checkbox_appearance_test.py b/tests/checkbox_appearance_test.py index fd1436e..2810848 100644 --- a/tests/checkbox_appearance_test.py +++ b/tests/checkbox_appearance_test.py @@ -35,7 +35,8 @@ def test_checkbox_appearance_streams_are_indirect_and_flatten_checked_state(tmp_ assert "/BBox" in appearance assert "/Resources" in appearance - assert normal_appearances["/Off"].get_data() == b"" + # pypdf versions can preserve insignificant PDF whitespace in empty streams. + assert normal_appearances["/Off"].get_data().strip() == b"" checked_appearance = normal_appearances["/Yes"] checked_bbox = list(checked_appearance["/BBox"]) checked_resources = dict(checked_appearance["/Resources"]) From 3dfb63ee8344d4583896098914a1e0776f206177 Mon Sep 17 00:00:00 2001 From: Julfried Date: Sat, 8 Aug 2026 15:46:07 +0200 Subject: [PATCH 12/12] Rename form creator test --- tests/{checkbox_appearance_test.py => form_creator_test.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/{checkbox_appearance_test.py => form_creator_test.py} (100%) diff --git a/tests/checkbox_appearance_test.py b/tests/form_creator_test.py similarity index 100% rename from tests/checkbox_appearance_test.py rename to tests/form_creator_test.py