145 lines
5.1 KiB
Python
145 lines
5.1 KiB
Python
# SPDX-FileCopyrightText: 2026 geisserml <geisserml@gmail.com>
|
|
# SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause
|
|
|
|
__all__ = ("PdfAttachment", )
|
|
|
|
import ctypes
|
|
from codecs import decode
|
|
import pypdfium2.raw as pdfium_c
|
|
import pypdfium2.internal as pdfium_i
|
|
from pypdfium2.internal import FPDF_WCHAR_size
|
|
from pypdfium2._helpers.misc import PdfiumError
|
|
|
|
|
|
def _encode_key(key):
|
|
if isinstance(key, str):
|
|
return (key + "\x00").encode("utf-8")
|
|
else:
|
|
raise TypeError(f"Key must be str, but {type(key).__name__} was given.")
|
|
|
|
|
|
class PdfAttachment (pdfium_i.AutoCastable):
|
|
"""
|
|
Attachment helper class.
|
|
See PDF 1.7, Section 7.11 "File Specifications".
|
|
|
|
Attributes:
|
|
raw (FPDF_ATTACHMENT):
|
|
The underlying PDFium attachment handle.
|
|
pdf (PdfDocument):
|
|
Reference to the document this attachment belongs to. Must remain valid as long as the attachment is used.
|
|
"""
|
|
|
|
# TODO consider using AutoCloseable machienery to guarantee `pdf` remains alive as long as the attachment object exists
|
|
|
|
# Problems with PDFium's attachment API:
|
|
# - https://crbug.com/pdfium/1939
|
|
# - https://crbug.com/pdfium/893
|
|
|
|
|
|
def __init__(self, raw, pdf):
|
|
self.raw = raw
|
|
self.pdf = pdf
|
|
|
|
|
|
def get_name(self):
|
|
"""
|
|
Returns:
|
|
str: Name of the attachment.
|
|
"""
|
|
n_bytes = pdfium_c.FPDFAttachment_GetName(self, None, 0)
|
|
n_units = -(n_bytes // -FPDF_WCHAR_size) # ceildiv
|
|
buffer = (pdfium_c.FPDF_WCHAR * n_units)()
|
|
pdfium_c.FPDFAttachment_GetName(self, buffer, n_bytes)
|
|
return decode(memoryview(buffer)[:n_units-1], "utf-16-le")
|
|
|
|
|
|
def get_data(self):
|
|
"""
|
|
Returns:
|
|
ctypes.Array: The attachment's file data (as :class:`~ctypes.c_char` array).
|
|
"""
|
|
|
|
n_bytes = ctypes.c_ulong()
|
|
pdfium_c.FPDFAttachment_GetFile(self, None, 0, n_bytes)
|
|
n_bytes = n_bytes.value
|
|
if n_bytes == 0:
|
|
raise PdfiumError(f"Failed to extract attachment (buffer length {n_bytes}).")
|
|
|
|
buffer = ctypes.create_string_buffer(n_bytes)
|
|
out_buflen = ctypes.c_ulong()
|
|
ok = pdfium_c.FPDFAttachment_GetFile(self, buffer, n_bytes, out_buflen)
|
|
out_buflen = out_buflen.value
|
|
if not ok:
|
|
raise PdfiumError("Failed to extract attachment (error status).")
|
|
if n_bytes < out_buflen:
|
|
raise PdfiumError(f"Failed to extract attachment (expected {n_bytes} bytes, but got {out_buflen}).")
|
|
|
|
return buffer
|
|
|
|
|
|
def set_data(self, data):
|
|
"""
|
|
Set the attachment's file data.
|
|
If this function is called on an existing attachment, it will be changed to point at the new data,
|
|
but the previous data will not be removed from the file (as of PDFium 5418).
|
|
|
|
Parameters:
|
|
data (bytes | ctypes.Array):
|
|
New file data for the attachment. May be any data type that can be implicitly converted to :class:`~ctypes.c_void_p`.
|
|
"""
|
|
ok = pdfium_c.FPDFAttachment_SetFile(self, self.pdf, data, len(data))
|
|
if not ok:
|
|
raise PdfiumError("Failed to set attachment data.")
|
|
|
|
|
|
def has_key(self, key):
|
|
"""
|
|
Parameters:
|
|
key (str):
|
|
A key to look for in the attachment's params dictionary.
|
|
Returns:
|
|
bool: True if *key* is contained in the params dictionary, False otherwise.
|
|
"""
|
|
return pdfium_c.FPDFAttachment_HasKey(self, _encode_key(key))
|
|
|
|
|
|
def get_value_type(self, key):
|
|
"""
|
|
Returns:
|
|
int: Type of the value of *key* in the params dictionary (:attr:`FPDF_OBJECT_*`).
|
|
"""
|
|
return pdfium_c.FPDFAttachment_GetValueType(self, _encode_key(key))
|
|
|
|
|
|
def get_str_value(self, key):
|
|
"""
|
|
Returns:
|
|
str: The value of *key* in the params dictionary, if it is a string or name.
|
|
Otherwise, an empty string will be returned. On other failures, an exception will be raised.
|
|
"""
|
|
|
|
enc_key = _encode_key(key)
|
|
n_bytes = pdfium_c.FPDFAttachment_GetStringValue(self, enc_key, None, 0)
|
|
if n_bytes <= 0:
|
|
raise PdfiumError(f"Failed to get value of key '{key}'.")
|
|
|
|
n_units = -(n_bytes // -FPDF_WCHAR_size) # ceildiv
|
|
buffer = (pdfium_c.FPDF_WCHAR * n_units)()
|
|
pdfium_c.FPDFAttachment_GetStringValue(self, enc_key, buffer, n_bytes)
|
|
|
|
return decode(memoryview(buffer)[:n_units-1], "utf-16-le")
|
|
|
|
|
|
def set_str_value(self, key, value):
|
|
"""
|
|
Set the attribute specified by *key* to the string *value*.
|
|
|
|
Parameters:
|
|
value (str): New string value for the attribute.
|
|
"""
|
|
enc_value = (value + "\x00").encode("utf-16-le")
|
|
enc_value_ptr = ctypes.cast(enc_value, pdfium_c.FPDF_WIDESTRING)
|
|
ok = pdfium_c.FPDFAttachment_SetStringValue(self, _encode_key(key), enc_value_ptr)
|
|
if not ok:
|
|
raise PdfiumError(f"Failed to set attachment param '{key}' to '{value}'.")
|