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
138 changes: 72 additions & 66 deletions pypdf/_doc_common.py

Large diffs are not rendered by default.

166 changes: 83 additions & 83 deletions pypdf/_page.py

Large diffs are not rendered by default.

20 changes: 11 additions & 9 deletions pypdf/_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
skip_over_comment,
skip_over_whitespace,
)
from .constants import TrailerKeys as TK
from .constants import TrailerKeys
from .errors import (
EmptyFileError,
FileNotDecryptedError,
Expand Down Expand Up @@ -190,7 +190,7 @@ def _handle_encryption(self, password: str | bytes | None) -> None:
# Some documents may not have a /ID, use two empty
# byte strings instead. Solves
# https://github.com/py-pdf/pypdf/issues/608
id_entry = self.trailer.get(TK.ID, NullObject()).get_object()
id_entry = self.trailer.get(TrailerKeys.ID, NullObject()).get_object()
id1_entry = b""
if isinstance(id_entry, ArrayObject) and id_entry:
first_id_entry = id_entry[0].get_object()
Expand All @@ -200,7 +200,7 @@ def _handle_encryption(self, password: str | bytes | None) -> None:
logger_warning("Ignoring non-string first /ID entry of the trailer", source=__name__)
elif not isinstance(id_entry, NullObject):
logger_warning("Ignoring malformed /ID entry of the trailer", source=__name__)
encrypt_entry = self.trailer[TK.ENCRYPT]
encrypt_entry = self.trailer[TrailerKeys.ENCRYPT]
if not isinstance(encrypt_entry, DictionaryObject):
raise PdfReadError("/Encrypt entry of the trailer is not a dictionary")
self._encryption = Encryption.read(encrypt_entry, id1_entry)
Expand Down Expand Up @@ -242,7 +242,7 @@ def root_object(self) -> DictionaryObject:
"""Provide access to "/Root". Standardized with PdfWriter."""
if self._validated_root:
return self._validated_root
root = self.trailer.get(TK.ROOT, NullObject()).get_object()
root = self.trailer.get(TrailerKeys.ROOT, NullObject()).get_object()
root_dictionary: DictionaryObject
if isinstance(root, DictionaryObject):
root_dictionary = root
Expand Down Expand Up @@ -302,7 +302,7 @@ def _info(self) -> DictionaryObject | None:
/Info Dictionary; None if the entry does not exist

"""
info = self.trailer.get(TK.INFO, None)
info = self.trailer.get(TrailerKeys.INFO, None)
if is_null_or_none(info):
return None
assert info is not None, "mypy"
Expand All @@ -322,7 +322,7 @@ def _ID(self) -> ArrayObject | None:
/ID array; None if the entry does not exist

"""
file_identifiers = self.trailer.get(TK.ID, None)
file_identifiers = self.trailer.get(TrailerKeys.ID, None)
if is_null_or_none(file_identifiers):
return None
assert file_identifiers is not None, "mypy"
Expand Down Expand Up @@ -1248,7 +1248,7 @@ def _read_xref_tables_and_trailers(
try:
xrefstream = self._read_pdf15_xref_stream(stream)
except Exception as exception: # pragma: no cover
if TK.ROOT in self.trailer:
if TrailerKeys.ROOT in self.trailer:
logger_warning(
"Previous trailer cannot be read: %(args)s",
source=__name__,
Expand All @@ -1266,7 +1266,9 @@ def _read_xref_tables_and_trailers(

# The trailer keys a PDF 1.5+ cross-reference stream carries in place of a
# `trailer` keyword (PDF 2.0 specification, table 17).
_XREF_STREAM_TRAILER_KEYS = (TK.ROOT, TK.ENCRYPT, TK.INFO, TK.ID, TK.SIZE)
_XREF_STREAM_TRAILER_KEYS = (
TrailerKeys.ROOT, TrailerKeys.ENCRYPT, TrailerKeys.INFO, TrailerKeys.ID, TrailerKeys.SIZE
)

def _process_xref_stream(self, xrefstream: DictionaryObject) -> None:
"""Process and handle the xref stream."""
Expand Down Expand Up @@ -1720,7 +1722,7 @@ def is_encrypted(self) -> bool:
Note that this property, if true, will remain true even after the
:meth:`decrypt()<pypdf.PdfReader.decrypt>` method is called.
"""
return TK.ENCRYPT in self.trailer
return TrailerKeys.ENCRYPT in self.trailer

def add_form_topname(self, name: str) -> DictionaryObject | None:
"""
Expand Down
98 changes: 49 additions & 49 deletions pypdf/_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,21 +67,21 @@
logger_warning,
)
from .actions import Action
from .constants import AnnotationDictionaryAttributes as AA
from .constants import (
AnnotationDictionaryAttributes,
CatalogAttributes,
Core,
FieldDictionaryAttributes,
GoToActionArguments,
ImageType,
InteractiveFormDictEntries,
OutlineFontFlag,
PageAttributes,
PageLabelStyle,
PagesAttributes,
TrailerKeys,
UserAccessPermissions,
)
from .constants import FieldDictionaryAttributes as FA
from .constants import PageAttributes as PG
from .constants import TrailerKeys as TK
from .errors import LimitReachedError, PdfReadError, PyPdfError
from .generic import (
PAGE_FIT,
Expand Down Expand Up @@ -463,9 +463,9 @@ def _add_object(self, obj: PdfObject) -> IndirectObject:
return obj.indirect_reference # type: ignore[return-value]
# check for /Contents in Pages (/Contents in annotations are strings)
if isinstance(obj, DictionaryObject) and isinstance(
obj.get(PG.CONTENTS, None), (ArrayObject, DictionaryObject)
obj.get(PageAttributes.CONTENTS, None), (ArrayObject, DictionaryObject)
):
obj[NameObject(PG.CONTENTS)] = self._add_object(obj[PG.CONTENTS])
obj[NameObject(PageAttributes.CONTENTS)] = self._add_object(obj[PageAttributes.CONTENTS])
self._objects.append(obj)
obj.indirect_reference = IndirectObject(len(self._objects), 0, self)
return obj.indirect_reference
Expand Down Expand Up @@ -916,7 +916,7 @@ def _add_apstream_object(
x_offset: The horizontal offset for the appearance stream.
y_offset: The vertical offset for the appearance stream.
"""
pg_res = cast(DictionaryObject, page[PG.RESOURCES])
pg_res = cast(DictionaryObject, page[PageAttributes.RESOURCES])
# Always add the resolved stream object to the writer to get a new IndirectObject.
# This ensures we have a valid IndirectObject managed by *this* writer.
xobject_ref = self._add_object(appearance_stream_obj)
Expand All @@ -936,13 +936,13 @@ def _add_apstream_object(
xobject_drawing_commands = f"q\n{xobject_cm._to_cm()}\n{xobject_name} Do\nQ".encode()
self._merge_content_stream_to_page(page, xobject_drawing_commands)

FFBITS_NUL = FA.FfBits(0)
FFBITS_NUL = FieldDictionaryAttributes.FfBits(0)

def update_page_form_field_values(
self,
page: PageObject | list[PageObject] | None,
fields: Mapping[str, str | list[str] | tuple[str, str, float]],
flags: FA.FfBits = FFBITS_NUL,
flags: FieldDictionaryAttributes.FfBits = FFBITS_NUL,
auto_regenerate: bool | None = True,
flatten: bool = False,
) -> None:
Expand Down Expand Up @@ -988,27 +988,27 @@ def update_page_form_field_values(
page = list(self.pages)
if isinstance(page, list):
for p in page:
if PG.ANNOTS in p: # just to prevent warnings
if PageAttributes.ANNOTS in p: # just to prevent warnings
self.update_page_form_field_values(p, fields, flags, None, flatten=flatten)
return
if PG.ANNOTS not in page:
if PageAttributes.ANNOTS not in page:
logger_warning("No fields to update on this page", source=__name__)
return
appearance_stream_obj: StreamObject | None = None

for annotation in page[PG.ANNOTS]: # type: ignore[attr-defined]
for annotation in page[PageAttributes.ANNOTS]: # type: ignore[attr-defined]
annotation = cast(DictionaryObject, annotation.get_object())
if annotation.get("/Subtype", "") != "/Widget":
continue
if "/FT" in annotation and "/T" in annotation:
parent_annotation = annotation
else:
parent_annotation = annotation.get(
PG.PARENT, DictionaryObject()
PageAttributes.PARENT, DictionaryObject()
).get_object()

for field, value in fields.items():
rectangle = cast(RectangleObject, annotation[AA.Rect])
rectangle = cast(RectangleObject, annotation[AnnotationDictionaryAttributes.Rect])
if not (
self._get_qualified_field_name(parent=parent_annotation) == field
or parent_annotation.get("/T", None) == field
Expand All @@ -1020,35 +1020,35 @@ def update_page_form_field_values(
):
del parent_annotation["/I"]
if flags:
annotation[NameObject(FA.Ff)] = NumberObject(flags)
annotation[NameObject(FieldDictionaryAttributes.Ff)] = NumberObject(flags)
# Set the field value
if not (value is None and flatten): # Only change values if given by user and not flattening.
if isinstance(value, list):
lst = ArrayObject(TextStringObject(v) for v in value)
parent_annotation[NameObject(FA.V)] = lst
parent_annotation[NameObject(FieldDictionaryAttributes.V)] = lst
elif isinstance(value, tuple):
parent_annotation[NameObject(FA.V)] = TextStringObject(
parent_annotation[NameObject(FieldDictionaryAttributes.V)] = TextStringObject(
value[0],
)
else:
parent_annotation[NameObject(FA.V)] = TextStringObject(value)
parent_annotation[NameObject(FieldDictionaryAttributes.V)] = TextStringObject(value)
# Get or create the field's appearance stream object
if parent_annotation.get(FA.FT) == "/Btn":
if parent_annotation.get(FieldDictionaryAttributes.FT) == "/Btn":
# Checkbox button (no /FT found in Radio widgets);
# We can find the associated appearance stream object
# within the annotation.
v = NameObject(value)
ap = cast(DictionaryObject, annotation[NameObject(AA.AP)])
ap = cast(DictionaryObject, annotation[NameObject(AnnotationDictionaryAttributes.AP)])
normal_ap = cast(DictionaryObject, ap["/N"])
if v not in normal_ap:
v = NameObject("/Off")
appearance_stream_obj = normal_ap.get(v)
# Other cases will be updated through the for loop
annotation[NameObject(AA.AS)] = v
annotation[NameObject(FA.V)] = v
annotation[NameObject(AnnotationDictionaryAttributes.AS)] = v
annotation[NameObject(FieldDictionaryAttributes.V)] = v
elif (
parent_annotation.get(FA.FT) == "/Tx"
or parent_annotation.get(FA.FT) == "/Ch"
parent_annotation.get(FieldDictionaryAttributes.FT) == "/Tx"
or parent_annotation.get(FieldDictionaryAttributes.FT) == "/Ch"
):
# Textbox; we need to generate the appearance stream object
if isinstance(value, tuple):
Expand All @@ -1060,20 +1060,20 @@ def update_page_form_field_values(
self, page, flatten, acro_form, parent_annotation, annotation
)
# Add the appearance stream object
if AA.AP not in annotation:
annotation[NameObject(AA.AP)] = DictionaryObject(
if AnnotationDictionaryAttributes.AP not in annotation:
annotation[NameObject(AnnotationDictionaryAttributes.AP)] = DictionaryObject(
{NameObject("/N"): self._add_object(appearance_stream_obj)}
)
elif "/N" not in (ap:= cast(DictionaryObject, annotation[AA.AP])):
cast(DictionaryObject, annotation[NameObject(AA.AP)])[
elif "/N" not in (ap:= cast(DictionaryObject, annotation[AnnotationDictionaryAttributes.AP])):
cast(DictionaryObject, annotation[NameObject(AnnotationDictionaryAttributes.AP)])[
NameObject("/N")
] = self._add_object(appearance_stream_obj)
else: # [/AP][/N] exists
n = annotation[AA.AP]["/N"].indirect_reference.idnum # type: ignore[index]
n = annotation[AnnotationDictionaryAttributes.AP]["/N"].indirect_reference.idnum # type: ignore[index]
self._objects[n - 1] = appearance_stream_obj
appearance_stream_obj.indirect_reference = IndirectObject(n, 0, self)
elif (
annotation.get(FA.FT) == "/Sig"
annotation.get(FieldDictionaryAttributes.FT) == "/Sig"
): # deprecated # not implemented yet
logger_warning("Signature forms not implemented yet", source=__name__)

Expand Down Expand Up @@ -1498,10 +1498,10 @@ def _write_increment(self, stream: StreamType) -> None:
self._info.indirect_reference.idnum - 1 # type: ignore[union-attr]
]
):
init_data[NameObject(TK.INFO)] = self._info.indirect_reference
init_data[NameObject(TK.PREV)] = NumberObject(self._reader._startxref)
init_data[NameObject(TrailerKeys.INFO)] = self._info.indirect_reference
init_data[NameObject(TrailerKeys.PREV)] = NumberObject(self._reader._startxref)
if self._ID:
init_data[NameObject(TK.ID)] = self._ID
init_data[NameObject(TrailerKeys.ID)] = self._ID
xr = StreamObject.initialize_from_dictionary(init_data)
xr.set_data(
b"".join(
Expand Down Expand Up @@ -1558,16 +1558,16 @@ def _write_trailer(self, stream: StreamType, xref_location: int) -> None:
stream.write(b"trailer\n")
trailer = DictionaryObject(
{
NameObject(TK.SIZE): NumberObject(len(self._objects) + 1),
NameObject(TK.ROOT): self.root_object.indirect_reference,
NameObject(TrailerKeys.SIZE): NumberObject(len(self._objects) + 1),
NameObject(TrailerKeys.ROOT): self.root_object.indirect_reference,
}
)
if self._info is not None:
trailer[NameObject(TK.INFO)] = self._info.indirect_reference
trailer[NameObject(TrailerKeys.INFO)] = self._info.indirect_reference
if self._ID is not None:
trailer[NameObject(TK.ID)] = self._ID
trailer[NameObject(TrailerKeys.ID)] = self._ID
if self._encrypt_entry:
trailer[NameObject(TK.ENCRYPT)] = self._encrypt_entry.indirect_reference
trailer[NameObject(TrailerKeys.ENCRYPT)] = self._encrypt_entry.indirect_reference
trailer.write_to_stream(stream)
stream.write(f"\nstartxref\n{xref_location}\n%%EOF\n".encode()) # eof

Expand Down Expand Up @@ -1975,7 +1975,7 @@ def add_named_destination(
"""
page_ref = self._get_page_reference(page_number)
if fit is None:
top = cast(DictionaryObject, page_ref.get_object()).get(PG.MEDIABOX)
top = cast(DictionaryObject, page_ref.get_object()).get(PageAttributes.MEDIABOX)
top = RectangleObject(top).top if top is not None else 0
fit = Fit.fit_horizontally(top=top)
if not isinstance(title, TextStringObject):
Expand Down Expand Up @@ -2019,8 +2019,8 @@ def _remove_annots_from_page(
subtypes: Iterable[str] | None,
) -> None:
page = cast(DictionaryObject, page.get_object())
if PG.ANNOTS in page:
annotations = page[PG.ANNOTS]
if PageAttributes.ANNOTS in page:
annotations = page[PageAttributes.ANNOTS]
if not isinstance(annotations, ArrayObject):
logger_warning(
"Annotations are not an array: %(annotations)s",
Expand Down Expand Up @@ -2381,21 +2381,21 @@ def add_uri(
lnk = DictionaryObject()
lnk.update(
{
NameObject(AA.Type): NameObject("/Annot"),
NameObject(AA.Subtype): NameObject("/Link"),
NameObject(AA.P): page_link,
NameObject(AA.Rect): rect,
NameObject(AnnotationDictionaryAttributes.Type): NameObject("/Annot"),
NameObject(AnnotationDictionaryAttributes.Subtype): NameObject("/Link"),
NameObject(AnnotationDictionaryAttributes.P): page_link,
NameObject(AnnotationDictionaryAttributes.Rect): rect,
NameObject("/H"): NameObject("/I"),
NameObject(AA.Border): ArrayObject(border_arr),
NameObject(AnnotationDictionaryAttributes.Border): ArrayObject(border_arr),
NameObject("/A"): lnk2,
}
)
lnk_ref = self._add_object(lnk)

if PG.ANNOTS in page_ref:
page_ref[PG.ANNOTS].append(lnk_ref)
if PageAttributes.ANNOTS in page_ref:
page_ref[PageAttributes.ANNOTS].append(lnk_ref)
else:
page_ref[NameObject(PG.ANNOTS)] = ArrayObject([lnk_ref])
page_ref[NameObject(PageAttributes.ANNOTS)] = ArrayObject([lnk_ref])

_valid_layouts = (
"/NoLayout",
Expand Down
Loading
Loading