264 lines
12 KiB
Python
264 lines
12 KiB
Python
# SPDX-FileCopyrightText: 2026 geisserml <geisserml@gmail.com>
|
|
# SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause
|
|
|
|
__all__ = ("PdfSysfontBase", "PdfDefaultTTFMap")
|
|
|
|
import sys
|
|
import ctypes
|
|
import atexit
|
|
import logging
|
|
import pypdfium2.raw as pdfium_c
|
|
import pypdfium2.internal as pdfium_i
|
|
from pypdfium2._helpers.misc import PdfiumError
|
|
from pypdfium2._lazy import cached_property, cached_property_clear
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class _DefaultSysfontInfoClass (pdfium_i.AutoCastable):
|
|
|
|
def __init__(self):
|
|
self._is_loaded = False
|
|
|
|
@cached_property
|
|
def raw(self):
|
|
logger.debug("Load default sysfont info")
|
|
default_ptr = pdfium_c.FPDF_GetDefaultSystemFontInfo()
|
|
if not default_ptr:
|
|
raise PdfiumError(f"No default FPDF_SYSFONTINFO available on this platform ({sys.platform!r}), cannot use {type(self).__name__}.")
|
|
self._is_loaded = True
|
|
# trust in python to invoke exit handlers in reverse order to creation
|
|
# this goes before any PdfSysfontBase atexit.register(), so it should only ever be closed after the sysfontinfo which relies on this default to remain valid
|
|
atexit.register(self._close_impl)
|
|
pdfium_i.ObjectTracker[None].add(self._wref_to_self)
|
|
return default_ptr.contents
|
|
|
|
def _close_impl(self):
|
|
if not self._is_loaded:
|
|
return
|
|
pdfium_i._debug_close("Free default sysfont info")
|
|
pdfium_c.FPDF_FreeDefaultSystemFontInfo(self.raw)
|
|
cached_property_clear(self, "raw")
|
|
pdfium_i.ObjectTracker[None].remove(self._wref_to_self)
|
|
self._is_loaded = False
|
|
|
|
def close(self):
|
|
atexit.unregister(self._close_impl)
|
|
self._close_impl()
|
|
|
|
_DefaultSysfontInfo = _DefaultSysfontInfoClass()
|
|
|
|
|
|
class _DefaultTTFMapClass:
|
|
|
|
@cached_property
|
|
def value(self):
|
|
# logger.debug("Retrieving default TT Font map...")
|
|
count = pdfium_c.FPDF_GetDefaultTTFMapCount()
|
|
map = {}
|
|
for i in range(count):
|
|
entry = pdfium_c.FPDF_GetDefaultTTFMapEntry(i).contents
|
|
map[entry.charset] = ctypes.cast(entry.fontname, ctypes.c_char_p).value # TODO decode
|
|
return map
|
|
|
|
def get(self, key, default=None):
|
|
out = self.value.get(key, default)
|
|
self.get = self.value.get # optimize away layer of indirection
|
|
return out
|
|
|
|
# FIXME sphinx docs don't support singleton pattern properly?
|
|
PdfDefaultTTFMap = _DefaultTTFMapClass()
|
|
"""
|
|
This object exposes the default TT Font map used by pdfium.\n
|
|
Access the ``.value`` cached property to obtain the map, or call ``.get(charset)``.
|
|
|
|
Note:
|
|
There is no guarantee as to whether a default font is installed or not.
|
|
If not installed, a system or internal substitute may be chosen by pdfium. |br|
|
|
``DEBUG_SYSFONTS=1 pypdfium2 default-fonts`` should give you some idea about substitution.
|
|
"""
|
|
|
|
|
|
_CallbackNames = ("Release", "EnumFonts", "MapFont", "GetFont", "GetFontData", "GetFaceName", "GetFontCharset", "DeleteFont")
|
|
|
|
class PdfSysfontBase (pdfium_i.AutoCastable):
|
|
"""
|
|
Base helper class to create a ``FPDF_SYSFONTINFO`` callback system.
|
|
Callbacks can be implemented by subclassing (see `fpdf_sysfontinfo.h` for available callouts and documentation).
|
|
|
|
This constructor merely creates the underlying ``FPDF_SYSFONTINFO`` instance.
|
|
Call :meth:`.setup` to actually register it with pdfium.
|
|
|
|
System font handlers may wrap another implementation, by default the root implementation provided by pdfium.
|
|
When a callback is not implemented, it will be automatically delegated to the default handler.
|
|
See the example below for how to invoke the default handler in a callback:
|
|
|
|
.. code-block:: python
|
|
|
|
class MySysfontImpl (PdfSysfontBase):
|
|
# substitute CallbackName accordingly
|
|
def CallbackName(self, _, arg1, arg2, ...)
|
|
print("Wrap before")
|
|
# Important: Do not pass the _ argument here, that's a pointer to self.raw.
|
|
# Pass self.default instead. The C callback expects its own struct, not the wrapper.
|
|
out = self.default.CallbackName(self.default, arg1, arg2, ...)
|
|
print("Wrap after")
|
|
return out
|
|
|
|
Alternatively, if using subclassing, you may want\n
|
|
.. code-block:: python\n
|
|
out = super().CallbackName(_, arg1, arg2, ...)\n
|
|
when the next class in the MRO has an implementation that you want to call.
|
|
|
|
Parameters:
|
|
default (None | FPDF_SYSFONTINFO | PdfSysfontBase):
|
|
The sysfont handler to be wrapped. If None (the default), pdfium's root implementation will be used.
|
|
Otherwise, this can be either a raw ``FPDF_SYSFONTINFO`` or another :class:`.PdfSysfontBase` instance.
|
|
|
|
Note:
|
|
When another :class:`.PdfSysfontBase` is being wrapped, some tricks are applied to avoid overhead:\n
|
|
- Where wrapper and child share the same callback, the child method will be forwarded to the wrapper (so, as a side effect, even stacking instances of the same class would result in only one call).
|
|
- Also, only in the actual ``FPDF_SYSFONTINFO`` object are callbacks ever enclosed in their :func:`~ctypes.CFUNCTYPE`, whereas wrappers call the original function directly.
|
|
|
|
Attributes:
|
|
raw (FPDF_SYSFONTINFO):
|
|
The underlying ``FPDF_SYSFONTINFO`` interface struct implemented by this class. May wrap :attr:`.default`.
|
|
default (FPDF_SYSFONTINFO | PdfSysfontBase):
|
|
The sysfont handler being wrapped. Wrapper callbacks typically delegate the actual work to the default implementation.
|
|
version (int):
|
|
The ``FPDF_SYSFONTINFO`` struct version used. Matches :attr:`.default.version` and :attr:`.raw.version`.
|
|
This is provided for interface compatibility with ``FPDF_SYSFONTINFO``, so that :attr:`.default` can be either a raw struct or :class:`.PdfSysfontBase`.
|
|
"""
|
|
|
|
#: PdfSysfontBase | None: Currently registered sysfont handler, or None if no sysfont handler is installed. This is a class-level attribute.
|
|
SINGLETON = None
|
|
|
|
def __init__(self, default=None):
|
|
|
|
self._is_installed = False
|
|
self._reusable = None
|
|
self._destroyed = False
|
|
self._child = None
|
|
|
|
if default is None:
|
|
self.default = _DefaultSysfontInfo.raw
|
|
else:
|
|
self.default = default
|
|
if isinstance(self.default, PdfSysfontBase):
|
|
self._child = self.default
|
|
self._forward_default_callbacks()
|
|
|
|
self.version = self.default.version
|
|
self.raw = pdfium_c.FPDF_SYSFONTINFO()
|
|
self.raw.version = self.version
|
|
|
|
callbacks = {n: getattr(self, n) for n in _CallbackNames}
|
|
if self.version != 1: # as per docs
|
|
del callbacks["EnumFonts"]
|
|
pdfium_i.set_callbacks(self.raw, **callbacks)
|
|
|
|
|
|
def _forward_default_callbacks(self):
|
|
# for any callbacks that were not re-implemented, we forward from default to avoid needless python function calls
|
|
reference_class = type(self) # or really just PdfSysfontBase?
|
|
for cb_name in _CallbackNames:
|
|
candidate = getattr(self.default, cb_name)
|
|
if getattr(reference_class, cb_name) is candidate.__func__:
|
|
setattr(self, cb_name, candidate)
|
|
|
|
def _iterkids(self):
|
|
child = self._child
|
|
while child:
|
|
yield child
|
|
child = child._child
|
|
|
|
def setup(self, reusable=False):
|
|
"""
|
|
Install (activate) the sysfont handler.
|
|
|
|
Note:\n
|
|
Once this method has been called, the instance is (by default) kept alive until the end of session, through an exit handler.
|
|
To stop the sysfont handler earlier, call :meth:`.close`.\n
|
|
Sysfont handlers are singleton, i.e. only one handler can be active at a time.
|
|
When a new handler is installed, the previous handler (if any) is implicitly closed.
|
|
"""
|
|
|
|
if PdfSysfontBase.SINGLETON is not None:
|
|
logger.info(f"Installing a new {type(self).__name__} instance implicitly closes previous sysfont handler instance {PdfSysfontBase.SINGLETON}")
|
|
PdfSysfontBase.SINGLETON.close(reusable=True)
|
|
|
|
if any(h._destroyed for h in (self, *self._iterkids())):
|
|
raise PdfiumError("You cannot register a sysfontinfo that has been destroyed, whether directly or indirectly. Pass `reusable=True` on setup or closing of handlers as necessary. Singleton replacement can do this implicitly.")
|
|
|
|
# trust in python to keep any object members (self.raw, self.default) alive while the object itself is referenced
|
|
# note that the object may still be needed after it was closed if reusable=True has been set and it is being wrapped by another sysfont handler
|
|
pdfium_c.FPDF_SetSystemFontInfo(self.raw)
|
|
PdfSysfontBase.SINGLETON = self
|
|
self._is_installed = True
|
|
self._reusable = reusable
|
|
atexit.register(self._close_impl)
|
|
pdfium_i.ObjectTracker[None].add(self._wref_to_self)
|
|
|
|
|
|
def _close_impl(self):
|
|
if not self._is_installed:
|
|
return
|
|
pdfium_i._debug_close(f"Close sysfontinfo")
|
|
|
|
# propagate parent state across all children, direct or indirect
|
|
for child in self._iterkids():
|
|
child._reusable = self._reusable
|
|
|
|
pdfium_c.FPDF_SetSystemFontInfo(None)
|
|
if self._destroyed:
|
|
# Assuming pdfium's default impl was used. In the unlikely event that it was not used, this is a no-op.
|
|
_DefaultSysfontInfo.close()
|
|
PdfSysfontBase.SINGLETON = None
|
|
pdfium_i.ObjectTracker[None].remove(self._wref_to_self)
|
|
|
|
def close(self, reusable=None): # manual
|
|
"""
|
|
Manually close the sysfont handler.
|
|
This unregisters the exit handler and releases the sysfont handler immediately.
|
|
|
|
See the note above for how sysfont handler lifetime is managed by default.
|
|
|
|
Parameters:
|
|
reusable (bool):
|
|
If False (the default), closing will destroy pdfium's default handler, rendering any direct or indirect wrappers thereof unusable.
|
|
If True, however, the default handler will not be harmed, so the object can be reused, like re-installing it some time after closing, wrapping it in another object, or just preserving the default handler for a new :class:`.PdfSysfontBase` instance.
|
|
This is automatically set to True on singleton replacement, when the previous handler is implicitly closed (i.e. ownership of the default instance is transferred to the new handler).
|
|
"""
|
|
if reusable is not None:
|
|
self._reusable = reusable
|
|
atexit.unregister(self._close_impl)
|
|
self._close_impl()
|
|
|
|
def Release(self, _):
|
|
if self._reusable:
|
|
pdfium_i._debug_close(f"fontinfo::Release: skip because it is reusable")
|
|
return
|
|
pdfium_i._debug_close(f"fontinfo::Release: actually release (wrapped={self._child})")
|
|
self._destroyed = True
|
|
return self.default.Release(self.default)
|
|
|
|
def EnumFonts(self, _, pMapper):
|
|
return self.default.EnumFonts(self.default, pMapper)
|
|
|
|
def MapFont(self, _, weight, bItalic, charset, pitch_family, face, _ignored):
|
|
return self.default.MapFont(self.default, weight, bItalic, charset, pitch_family, face, _ignored)
|
|
|
|
def GetFont(self, _, face):
|
|
return self.default.GetFont(self.default, face)
|
|
|
|
def GetFontData(self, _, hFont, table, buffer, buf_size):
|
|
return self.default.GetFontData(self.default, hFont, table, buffer, buf_size)
|
|
|
|
def GetFaceName(self, _, hFont, buffer, buf_size):
|
|
return self.default.GetFaceName(self.default, hFont, buffer, buf_size)
|
|
|
|
def GetFontCharset(self, _, hFont):
|
|
return self.default.GetFontCharset(self.default, hFont)
|
|
|
|
def DeleteFont(self, _, hFont):
|
|
return self.default.DeleteFont(self.default, hFont)
|