Skip to content

Commit 9dcf60f

Browse files
authored
ENH: Allow writing/updating all properties of an embedded file (#3374)
Closes #3368.
1 parent 76759bd commit 9dcf60f

File tree

5 files changed

+531
-90
lines changed

5 files changed

+531
-90
lines changed

pypdf/_utils.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,31 @@ def parse_iso8824_date(text: Optional[str]) -> Optional[datetime]:
110110
raise ValueError(f"Can not convert date: {orgtext}")
111111

112112

113+
def format_iso8824_date(dt: datetime) -> str:
114+
"""
115+
Convert a datetime object to PDF date string format.
116+
117+
Converts datetime to the PDF date format D:YYYYMMDDHHmmSSOHH'mm
118+
as specified in the PDF Reference.
119+
120+
Args:
121+
dt: A datetime object to convert.
122+
123+
Returns:
124+
A date string in PDF format.
125+
"""
126+
date_str = dt.strftime("D:%Y%m%d%H%M%S")
127+
if dt.tzinfo is not None:
128+
offset = dt.utcoffset()
129+
assert offset is not None
130+
total_seconds = int(offset.total_seconds())
131+
hours, remainder = divmod(abs(total_seconds), 3600)
132+
minutes = remainder // 60
133+
sign = "+" if total_seconds >= 0 else "-"
134+
date_str += f"{sign}{hours:02d}'{minutes:02d}'"
135+
return date_str
136+
137+
113138
def _get_max_pdf_version_header(header1: str, header2: str) -> str:
114139
versions = (
115140
"%PDF-1.3",

pypdf/_writer.py

Lines changed: 6 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,6 @@
7171
from .constants import CatalogAttributes as CA
7272
from .constants import (
7373
CatalogDictionary,
74-
FileSpecificationDictionaryEntries,
7574
GoToActionArguments,
7675
ImageType,
7776
InteractiveFormDictEntries,
@@ -95,6 +94,7 @@
9594
DecodedStreamObject,
9695
Destination,
9796
DictionaryObject,
97+
EmbeddedFile,
9898
Fit,
9999
FloatObject,
100100
IndirectObject,
@@ -741,7 +741,7 @@ def add_js(self, javascript: str) -> None:
741741
)
742742
js_list.append(self._add_object(js))
743743

744-
def add_attachment(self, filename: str, data: Union[str, bytes]) -> None:
744+
def add_attachment(self, filename: str, data: Union[str, bytes]) -> "EmbeddedFile":
745745
"""
746746
Embed a file inside the PDF.
747747
@@ -753,85 +753,11 @@ def add_attachment(self, filename: str, data: Union[str, bytes]) -> None:
753753
filename: The filename to display.
754754
data: The data in the file.
755755
756-
"""
757-
# We need three entries:
758-
# * The file's data
759-
# * The /Filespec entry
760-
# * The file's name, which goes in the Catalog
761-
762-
# The entry for the file
763-
# Sample:
764-
# 8 0 obj
765-
# <<
766-
# /Length 12
767-
# /Type /EmbeddedFile
768-
# >>
769-
# stream
770-
# Hello world!
771-
# endstream
772-
# endobj
773-
774-
if isinstance(data, str):
775-
data = data.encode("latin-1")
776-
file_entry = DecodedStreamObject()
777-
file_entry.set_data(data)
778-
file_entry.update({NameObject(PA.TYPE): NameObject("/EmbeddedFile")})
779-
780-
# The Filespec entry
781-
# Sample:
782-
# 7 0 obj
783-
# <<
784-
# /Type /Filespec
785-
# /F (hello.txt)
786-
# /EF << /F 8 0 R >>
787-
# >>
788-
# endobj
789-
790-
ef_entry = DictionaryObject()
791-
ef_entry.update({NameObject("/F"): self._add_object(file_entry)})
792-
793-
filespec = DictionaryObject()
794-
filespec.update(
795-
{
796-
NameObject(PA.TYPE): NameObject("/Filespec"),
797-
NameObject(FileSpecificationDictionaryEntries.F): create_string_object(
798-
filename
799-
), # Perhaps also try TextStringObject
800-
NameObject(FileSpecificationDictionaryEntries.EF): ef_entry,
801-
}
802-
)
756+
Returns:
757+
EmbeddedFile instance for the newly created embedded file.
803758
804-
# Then create the entry for the root, as it needs
805-
# a reference to the Filespec
806-
# Sample:
807-
# 1 0 obj
808-
# <<
809-
# /Type /Catalog
810-
# /Outlines 2 0 R
811-
# /Pages 3 0 R
812-
# /Names << /EmbeddedFiles << /Names [(hello.txt) 7 0 R] >> >>
813-
# >>
814-
# endobj
815-
816-
if CA.NAMES not in self._root_object:
817-
self._root_object[NameObject(CA.NAMES)] = self._add_object(
818-
DictionaryObject()
819-
)
820-
if "/EmbeddedFiles" not in cast(DictionaryObject, self._root_object[CA.NAMES]):
821-
embedded_files_names_dictionary = DictionaryObject(
822-
{NameObject(CA.NAMES): ArrayObject()}
823-
)
824-
cast(DictionaryObject, self._root_object[CA.NAMES])[
825-
NameObject("/EmbeddedFiles")
826-
] = self._add_object(embedded_files_names_dictionary)
827-
else:
828-
embedded_files_names_dictionary = cast(
829-
DictionaryObject,
830-
cast(DictionaryObject, self._root_object[CA.NAMES])["/EmbeddedFiles"],
831-
)
832-
cast(ArrayObject, embedded_files_names_dictionary[CA.NAMES]).extend(
833-
[create_string_object(filename), filespec]
834-
)
759+
"""
760+
return EmbeddedFile._create_new(self, filename, data)
835761

836762
def append_pages_from_reader(
837763
self,

0 commit comments

Comments
 (0)