Viewing File: /home/ubuntu/combine_ai/combine/lib/python3.10/site-packages/fitz_old/fitz_old.py
# This file was automatically generated by SWIG (https://www.swig.org).
# Version 4.2.1
#
# Do not make changes to this file unless you know what you are doing - modify
# the SWIG interface file instead.
from sys import version_info as _swig_python_version_info
# Import the low-level C/C++ module
if __package__ or "." in __name__:
from . import _fitz_old
else:
import _fitz_old
try:
import builtins as __builtin__
except ImportError:
import __builtin__
def _swig_repr(self):
try:
strthis = "proxy of " + self.this.__repr__()
except __builtin__.Exception:
strthis = ""
return "<%s.%s; %s >" % (self.__class__.__module__, self.__class__.__name__, strthis,)
def _swig_setattr_nondynamic_instance_variable(set):
def set_instance_attr(self, name, value):
if name == "this":
set(self, name, value)
elif name == "thisown":
self.this.own(value)
elif hasattr(self, name) and isinstance(getattr(type(self), name), property):
set(self, name, value)
else:
raise AttributeError("You cannot add instance attributes to %s" % self)
return set_instance_attr
def _swig_setattr_nondynamic_class_variable(set):
def set_class_attr(cls, name, value):
if hasattr(cls, name) and not isinstance(getattr(cls, name), property):
set(cls, name, value)
else:
raise AttributeError("You cannot add class attributes to %s" % cls)
return set_class_attr
def _swig_add_metaclass(metaclass):
"""Class decorator for adding a metaclass to a SWIG wrapped class - a slimmed down version of six.add_metaclass"""
def wrapper(cls):
return metaclass(cls.__name__, cls.__bases__, cls.__dict__.copy())
return wrapper
class _SwigNonDynamicMeta(type):
"""Meta class to enforce nondynamic attributes (no new attributes) for a class"""
__setattr__ = _swig_setattr_nondynamic_class_variable(type.__setattr__)
FZ_VERSION = _fitz_old.FZ_VERSION
FZ_VERSION_MAJOR = _fitz_old.FZ_VERSION_MAJOR
FZ_VERSION_MINOR = _fitz_old.FZ_VERSION_MINOR
FZ_VERSION_PATCH = _fitz_old.FZ_VERSION_PATCH
import sys
import io
import math
import os
import weakref
import hashlib
import typing
import binascii
import re
import tarfile
import zipfile
import pathlib
import string
# PDF names must not contain these characters:
INVALID_NAME_CHARS = set(string.whitespace + "()<>[]{}/%" + chr(0))
TESSDATA_PREFIX = os.getenv("TESSDATA_PREFIX")
point_like = "point_like"
rect_like = "rect_like"
matrix_like = "matrix_like"
quad_like = "quad_like"
AnyType = typing.Any
OptInt = typing.Union[int, None]
OptFloat = typing.Optional[float]
OptStr = typing.Optional[str]
OptDict = typing.Optional[dict]
OptBytes = typing.Optional[typing.ByteString]
OptSeq = typing.Optional[typing.Sequence]
try:
from pymupdf_fonts import fontdescriptors, fontbuffers
fitz_fontdescriptors = fontdescriptors.copy()
for k in fitz_fontdescriptors.keys():
fitz_fontdescriptors[k]["loader"] = fontbuffers[k]
del fontdescriptors, fontbuffers
except ImportError:
fitz_fontdescriptors = {}
VersionFitz = "1.24.1" # MuPDF version.
VersionBind = "1.24.1" # PyMuPDF version.
VersionDate = "2024-04-02 00:00:01"
version = (VersionBind, VersionFitz, "20240402000001")
pymupdf_version_tuple = tuple( [int(i) for i in VersionFitz.split('.')])
pymupdf_git_sha = '055832f66f26791de662a437b4660ad80e950c88'
pymupdf_git_comment = f'Update changelog, version numbers and release dates for release 1.24.1.'
pymupdf_git_diff = f''
pymupdf_git_branch = f'main'
mupdf_git_sha = '055832f66f26791de662a437b4660ad80e950c88'
mupdf_git_comment = f'Update changelog, version numbers and release dates for release 1.24.1.'
mupdf_git_diff = f''
mupdf_git_branch = f'main'
EPSILON = _fitz_old.EPSILON
PDF_ANNOT_TEXT = _fitz_old.PDF_ANNOT_TEXT
PDF_ANNOT_LINK = _fitz_old.PDF_ANNOT_LINK
PDF_ANNOT_FREE_TEXT = _fitz_old.PDF_ANNOT_FREE_TEXT
PDF_ANNOT_LINE = _fitz_old.PDF_ANNOT_LINE
PDF_ANNOT_SQUARE = _fitz_old.PDF_ANNOT_SQUARE
PDF_ANNOT_CIRCLE = _fitz_old.PDF_ANNOT_CIRCLE
PDF_ANNOT_POLYGON = _fitz_old.PDF_ANNOT_POLYGON
PDF_ANNOT_POLY_LINE = _fitz_old.PDF_ANNOT_POLY_LINE
PDF_ANNOT_HIGHLIGHT = _fitz_old.PDF_ANNOT_HIGHLIGHT
PDF_ANNOT_UNDERLINE = _fitz_old.PDF_ANNOT_UNDERLINE
PDF_ANNOT_SQUIGGLY = _fitz_old.PDF_ANNOT_SQUIGGLY
PDF_ANNOT_STRIKE_OUT = _fitz_old.PDF_ANNOT_STRIKE_OUT
PDF_ANNOT_REDACT = _fitz_old.PDF_ANNOT_REDACT
PDF_ANNOT_STAMP = _fitz_old.PDF_ANNOT_STAMP
PDF_ANNOT_CARET = _fitz_old.PDF_ANNOT_CARET
PDF_ANNOT_INK = _fitz_old.PDF_ANNOT_INK
PDF_ANNOT_POPUP = _fitz_old.PDF_ANNOT_POPUP
PDF_ANNOT_FILE_ATTACHMENT = _fitz_old.PDF_ANNOT_FILE_ATTACHMENT
PDF_ANNOT_SOUND = _fitz_old.PDF_ANNOT_SOUND
PDF_ANNOT_MOVIE = _fitz_old.PDF_ANNOT_MOVIE
PDF_ANNOT_RICH_MEDIA = _fitz_old.PDF_ANNOT_RICH_MEDIA
PDF_ANNOT_WIDGET = _fitz_old.PDF_ANNOT_WIDGET
PDF_ANNOT_SCREEN = _fitz_old.PDF_ANNOT_SCREEN
PDF_ANNOT_PRINTER_MARK = _fitz_old.PDF_ANNOT_PRINTER_MARK
PDF_ANNOT_TRAP_NET = _fitz_old.PDF_ANNOT_TRAP_NET
PDF_ANNOT_WATERMARK = _fitz_old.PDF_ANNOT_WATERMARK
PDF_ANNOT_3D = _fitz_old.PDF_ANNOT_3D
PDF_ANNOT_PROJECTION = _fitz_old.PDF_ANNOT_PROJECTION
PDF_ANNOT_UNKNOWN = _fitz_old.PDF_ANNOT_UNKNOWN
PDF_REDACT_IMAGE_NONE = _fitz_old.PDF_REDACT_IMAGE_NONE
PDF_REDACT_IMAGE_REMOVE = _fitz_old.PDF_REDACT_IMAGE_REMOVE
PDF_REDACT_IMAGE_PIXELS = _fitz_old.PDF_REDACT_IMAGE_PIXELS
PDF_ANNOT_IS_INVISIBLE = _fitz_old.PDF_ANNOT_IS_INVISIBLE
PDF_ANNOT_IS_HIDDEN = _fitz_old.PDF_ANNOT_IS_HIDDEN
PDF_ANNOT_IS_PRINT = _fitz_old.PDF_ANNOT_IS_PRINT
PDF_ANNOT_IS_NO_ZOOM = _fitz_old.PDF_ANNOT_IS_NO_ZOOM
PDF_ANNOT_IS_NO_ROTATE = _fitz_old.PDF_ANNOT_IS_NO_ROTATE
PDF_ANNOT_IS_NO_VIEW = _fitz_old.PDF_ANNOT_IS_NO_VIEW
PDF_ANNOT_IS_READ_ONLY = _fitz_old.PDF_ANNOT_IS_READ_ONLY
PDF_ANNOT_IS_LOCKED = _fitz_old.PDF_ANNOT_IS_LOCKED
PDF_ANNOT_IS_TOGGLE_NO_VIEW = _fitz_old.PDF_ANNOT_IS_TOGGLE_NO_VIEW
PDF_ANNOT_IS_LOCKED_CONTENTS = _fitz_old.PDF_ANNOT_IS_LOCKED_CONTENTS
PDF_ANNOT_LE_NONE = _fitz_old.PDF_ANNOT_LE_NONE
PDF_ANNOT_LE_SQUARE = _fitz_old.PDF_ANNOT_LE_SQUARE
PDF_ANNOT_LE_CIRCLE = _fitz_old.PDF_ANNOT_LE_CIRCLE
PDF_ANNOT_LE_DIAMOND = _fitz_old.PDF_ANNOT_LE_DIAMOND
PDF_ANNOT_LE_OPEN_ARROW = _fitz_old.PDF_ANNOT_LE_OPEN_ARROW
PDF_ANNOT_LE_CLOSED_ARROW = _fitz_old.PDF_ANNOT_LE_CLOSED_ARROW
PDF_ANNOT_LE_BUTT = _fitz_old.PDF_ANNOT_LE_BUTT
PDF_ANNOT_LE_R_OPEN_ARROW = _fitz_old.PDF_ANNOT_LE_R_OPEN_ARROW
PDF_ANNOT_LE_R_CLOSED_ARROW = _fitz_old.PDF_ANNOT_LE_R_CLOSED_ARROW
PDF_ANNOT_LE_SLASH = _fitz_old.PDF_ANNOT_LE_SLASH
PDF_WIDGET_TYPE_UNKNOWN = _fitz_old.PDF_WIDGET_TYPE_UNKNOWN
PDF_WIDGET_TYPE_BUTTON = _fitz_old.PDF_WIDGET_TYPE_BUTTON
PDF_WIDGET_TYPE_CHECKBOX = _fitz_old.PDF_WIDGET_TYPE_CHECKBOX
PDF_WIDGET_TYPE_COMBOBOX = _fitz_old.PDF_WIDGET_TYPE_COMBOBOX
PDF_WIDGET_TYPE_LISTBOX = _fitz_old.PDF_WIDGET_TYPE_LISTBOX
PDF_WIDGET_TYPE_RADIOBUTTON = _fitz_old.PDF_WIDGET_TYPE_RADIOBUTTON
PDF_WIDGET_TYPE_SIGNATURE = _fitz_old.PDF_WIDGET_TYPE_SIGNATURE
PDF_WIDGET_TYPE_TEXT = _fitz_old.PDF_WIDGET_TYPE_TEXT
PDF_WIDGET_TX_FORMAT_NONE = _fitz_old.PDF_WIDGET_TX_FORMAT_NONE
PDF_WIDGET_TX_FORMAT_NUMBER = _fitz_old.PDF_WIDGET_TX_FORMAT_NUMBER
PDF_WIDGET_TX_FORMAT_SPECIAL = _fitz_old.PDF_WIDGET_TX_FORMAT_SPECIAL
PDF_WIDGET_TX_FORMAT_DATE = _fitz_old.PDF_WIDGET_TX_FORMAT_DATE
PDF_WIDGET_TX_FORMAT_TIME = _fitz_old.PDF_WIDGET_TX_FORMAT_TIME
PDF_FIELD_IS_READ_ONLY = _fitz_old.PDF_FIELD_IS_READ_ONLY
PDF_FIELD_IS_REQUIRED = _fitz_old.PDF_FIELD_IS_REQUIRED
PDF_FIELD_IS_NO_EXPORT = _fitz_old.PDF_FIELD_IS_NO_EXPORT
PDF_TX_FIELD_IS_MULTILINE = _fitz_old.PDF_TX_FIELD_IS_MULTILINE
PDF_TX_FIELD_IS_PASSWORD = _fitz_old.PDF_TX_FIELD_IS_PASSWORD
PDF_TX_FIELD_IS_FILE_SELECT = _fitz_old.PDF_TX_FIELD_IS_FILE_SELECT
PDF_TX_FIELD_IS_DO_NOT_SPELL_CHECK = _fitz_old.PDF_TX_FIELD_IS_DO_NOT_SPELL_CHECK
PDF_TX_FIELD_IS_DO_NOT_SCROLL = _fitz_old.PDF_TX_FIELD_IS_DO_NOT_SCROLL
PDF_TX_FIELD_IS_COMB = _fitz_old.PDF_TX_FIELD_IS_COMB
PDF_TX_FIELD_IS_RICH_TEXT = _fitz_old.PDF_TX_FIELD_IS_RICH_TEXT
PDF_BTN_FIELD_IS_NO_TOGGLE_TO_OFF = _fitz_old.PDF_BTN_FIELD_IS_NO_TOGGLE_TO_OFF
PDF_BTN_FIELD_IS_RADIO = _fitz_old.PDF_BTN_FIELD_IS_RADIO
PDF_BTN_FIELD_IS_PUSHBUTTON = _fitz_old.PDF_BTN_FIELD_IS_PUSHBUTTON
PDF_BTN_FIELD_IS_RADIOS_IN_UNISON = _fitz_old.PDF_BTN_FIELD_IS_RADIOS_IN_UNISON
PDF_CH_FIELD_IS_COMBO = _fitz_old.PDF_CH_FIELD_IS_COMBO
PDF_CH_FIELD_IS_EDIT = _fitz_old.PDF_CH_FIELD_IS_EDIT
PDF_CH_FIELD_IS_SORT = _fitz_old.PDF_CH_FIELD_IS_SORT
PDF_CH_FIELD_IS_MULTI_SELECT = _fitz_old.PDF_CH_FIELD_IS_MULTI_SELECT
PDF_CH_FIELD_IS_DO_NOT_SPELL_CHECK = _fitz_old.PDF_CH_FIELD_IS_DO_NOT_SPELL_CHECK
PDF_CH_FIELD_IS_COMMIT_ON_SEL_CHANGE = _fitz_old.PDF_CH_FIELD_IS_COMMIT_ON_SEL_CHANGE
PDF_SIGNATURE_ERROR_OKAY = _fitz_old.PDF_SIGNATURE_ERROR_OKAY
PDF_SIGNATURE_ERROR_NO_SIGNATURES = _fitz_old.PDF_SIGNATURE_ERROR_NO_SIGNATURES
PDF_SIGNATURE_ERROR_NO_CERTIFICATE = _fitz_old.PDF_SIGNATURE_ERROR_NO_CERTIFICATE
PDF_SIGNATURE_ERROR_DIGEST_FAILURE = _fitz_old.PDF_SIGNATURE_ERROR_DIGEST_FAILURE
PDF_SIGNATURE_ERROR_SELF_SIGNED = _fitz_old.PDF_SIGNATURE_ERROR_SELF_SIGNED
PDF_SIGNATURE_ERROR_SELF_SIGNED_IN_CHAIN = _fitz_old.PDF_SIGNATURE_ERROR_SELF_SIGNED_IN_CHAIN
PDF_SIGNATURE_ERROR_NOT_TRUSTED = _fitz_old.PDF_SIGNATURE_ERROR_NOT_TRUSTED
PDF_SIGNATURE_ERROR_UNKNOWN = _fitz_old.PDF_SIGNATURE_ERROR_UNKNOWN
PDF_SIGNATURE_SHOW_LABELS = _fitz_old.PDF_SIGNATURE_SHOW_LABELS
PDF_SIGNATURE_SHOW_DN = _fitz_old.PDF_SIGNATURE_SHOW_DN
PDF_SIGNATURE_SHOW_DATE = _fitz_old.PDF_SIGNATURE_SHOW_DATE
PDF_SIGNATURE_SHOW_TEXT_NAME = _fitz_old.PDF_SIGNATURE_SHOW_TEXT_NAME
PDF_SIGNATURE_SHOW_GRAPHIC_NAME = _fitz_old.PDF_SIGNATURE_SHOW_GRAPHIC_NAME
PDF_SIGNATURE_SHOW_LOGO = _fitz_old.PDF_SIGNATURE_SHOW_LOGO
PDF_SIGNATURE_DEFAULT_APPEARANCE = _fitz_old.PDF_SIGNATURE_DEFAULT_APPEARANCE
CS_RGB = _fitz_old.CS_RGB
CS_GRAY = _fitz_old.CS_GRAY
CS_CMYK = _fitz_old.CS_CMYK
PDF_ENCRYPT_KEEP = _fitz_old.PDF_ENCRYPT_KEEP
PDF_ENCRYPT_NONE = _fitz_old.PDF_ENCRYPT_NONE
PDF_ENCRYPT_RC4_40 = _fitz_old.PDF_ENCRYPT_RC4_40
PDF_ENCRYPT_RC4_128 = _fitz_old.PDF_ENCRYPT_RC4_128
PDF_ENCRYPT_AES_128 = _fitz_old.PDF_ENCRYPT_AES_128
PDF_ENCRYPT_AES_256 = _fitz_old.PDF_ENCRYPT_AES_256
PDF_ENCRYPT_UNKNOWN = _fitz_old.PDF_ENCRYPT_UNKNOWN
PDF_PERM_PRINT = _fitz_old.PDF_PERM_PRINT
PDF_PERM_MODIFY = _fitz_old.PDF_PERM_MODIFY
PDF_PERM_COPY = _fitz_old.PDF_PERM_COPY
PDF_PERM_ANNOTATE = _fitz_old.PDF_PERM_ANNOTATE
PDF_PERM_FORM = _fitz_old.PDF_PERM_FORM
PDF_PERM_ACCESSIBILITY = _fitz_old.PDF_PERM_ACCESSIBILITY
PDF_PERM_ASSEMBLE = _fitz_old.PDF_PERM_ASSEMBLE
PDF_PERM_PRINT_HQ = _fitz_old.PDF_PERM_PRINT_HQ
PDF_BM_Color = _fitz_old.PDF_BM_Color
PDF_BM_ColorBurn = _fitz_old.PDF_BM_ColorBurn
PDF_BM_ColorDodge = _fitz_old.PDF_BM_ColorDodge
PDF_BM_Darken = _fitz_old.PDF_BM_Darken
PDF_BM_Difference = _fitz_old.PDF_BM_Difference
PDF_BM_Exclusion = _fitz_old.PDF_BM_Exclusion
PDF_BM_HardLight = _fitz_old.PDF_BM_HardLight
PDF_BM_Hue = _fitz_old.PDF_BM_Hue
PDF_BM_Lighten = _fitz_old.PDF_BM_Lighten
PDF_BM_Luminosity = _fitz_old.PDF_BM_Luminosity
PDF_BM_Multiply = _fitz_old.PDF_BM_Multiply
PDF_BM_Normal = _fitz_old.PDF_BM_Normal
PDF_BM_Overlay = _fitz_old.PDF_BM_Overlay
PDF_BM_Saturation = _fitz_old.PDF_BM_Saturation
PDF_BM_Screen = _fitz_old.PDF_BM_Screen
PDF_BM_SoftLight = _fitz_old.PDF_BM_SoftLight
TEXT_FONT_SUPERSCRIPT = _fitz_old.TEXT_FONT_SUPERSCRIPT
TEXT_FONT_ITALIC = _fitz_old.TEXT_FONT_ITALIC
TEXT_FONT_SERIFED = _fitz_old.TEXT_FONT_SERIFED
TEXT_FONT_MONOSPACED = _fitz_old.TEXT_FONT_MONOSPACED
TEXT_FONT_BOLD = _fitz_old.TEXT_FONT_BOLD
UCDN_SCRIPT_COMMON = _fitz_old.UCDN_SCRIPT_COMMON
UCDN_SCRIPT_LATIN = _fitz_old.UCDN_SCRIPT_LATIN
UCDN_SCRIPT_GREEK = _fitz_old.UCDN_SCRIPT_GREEK
UCDN_SCRIPT_CYRILLIC = _fitz_old.UCDN_SCRIPT_CYRILLIC
UCDN_SCRIPT_ARMENIAN = _fitz_old.UCDN_SCRIPT_ARMENIAN
UCDN_SCRIPT_HEBREW = _fitz_old.UCDN_SCRIPT_HEBREW
UCDN_SCRIPT_ARABIC = _fitz_old.UCDN_SCRIPT_ARABIC
UCDN_SCRIPT_SYRIAC = _fitz_old.UCDN_SCRIPT_SYRIAC
UCDN_SCRIPT_THAANA = _fitz_old.UCDN_SCRIPT_THAANA
UCDN_SCRIPT_DEVANAGARI = _fitz_old.UCDN_SCRIPT_DEVANAGARI
UCDN_SCRIPT_BENGALI = _fitz_old.UCDN_SCRIPT_BENGALI
UCDN_SCRIPT_GURMUKHI = _fitz_old.UCDN_SCRIPT_GURMUKHI
UCDN_SCRIPT_GUJARATI = _fitz_old.UCDN_SCRIPT_GUJARATI
UCDN_SCRIPT_ORIYA = _fitz_old.UCDN_SCRIPT_ORIYA
UCDN_SCRIPT_TAMIL = _fitz_old.UCDN_SCRIPT_TAMIL
UCDN_SCRIPT_TELUGU = _fitz_old.UCDN_SCRIPT_TELUGU
UCDN_SCRIPT_KANNADA = _fitz_old.UCDN_SCRIPT_KANNADA
UCDN_SCRIPT_MALAYALAM = _fitz_old.UCDN_SCRIPT_MALAYALAM
UCDN_SCRIPT_SINHALA = _fitz_old.UCDN_SCRIPT_SINHALA
UCDN_SCRIPT_THAI = _fitz_old.UCDN_SCRIPT_THAI
UCDN_SCRIPT_LAO = _fitz_old.UCDN_SCRIPT_LAO
UCDN_SCRIPT_TIBETAN = _fitz_old.UCDN_SCRIPT_TIBETAN
UCDN_SCRIPT_MYANMAR = _fitz_old.UCDN_SCRIPT_MYANMAR
UCDN_SCRIPT_GEORGIAN = _fitz_old.UCDN_SCRIPT_GEORGIAN
UCDN_SCRIPT_HANGUL = _fitz_old.UCDN_SCRIPT_HANGUL
UCDN_SCRIPT_ETHIOPIC = _fitz_old.UCDN_SCRIPT_ETHIOPIC
UCDN_SCRIPT_CHEROKEE = _fitz_old.UCDN_SCRIPT_CHEROKEE
UCDN_SCRIPT_CANADIAN_ABORIGINAL = _fitz_old.UCDN_SCRIPT_CANADIAN_ABORIGINAL
UCDN_SCRIPT_OGHAM = _fitz_old.UCDN_SCRIPT_OGHAM
UCDN_SCRIPT_RUNIC = _fitz_old.UCDN_SCRIPT_RUNIC
UCDN_SCRIPT_KHMER = _fitz_old.UCDN_SCRIPT_KHMER
UCDN_SCRIPT_MONGOLIAN = _fitz_old.UCDN_SCRIPT_MONGOLIAN
UCDN_SCRIPT_HIRAGANA = _fitz_old.UCDN_SCRIPT_HIRAGANA
UCDN_SCRIPT_KATAKANA = _fitz_old.UCDN_SCRIPT_KATAKANA
UCDN_SCRIPT_BOPOMOFO = _fitz_old.UCDN_SCRIPT_BOPOMOFO
UCDN_SCRIPT_HAN = _fitz_old.UCDN_SCRIPT_HAN
UCDN_SCRIPT_YI = _fitz_old.UCDN_SCRIPT_YI
UCDN_SCRIPT_OLD_ITALIC = _fitz_old.UCDN_SCRIPT_OLD_ITALIC
UCDN_SCRIPT_GOTHIC = _fitz_old.UCDN_SCRIPT_GOTHIC
UCDN_SCRIPT_DESERET = _fitz_old.UCDN_SCRIPT_DESERET
UCDN_SCRIPT_INHERITED = _fitz_old.UCDN_SCRIPT_INHERITED
UCDN_SCRIPT_TAGALOG = _fitz_old.UCDN_SCRIPT_TAGALOG
UCDN_SCRIPT_HANUNOO = _fitz_old.UCDN_SCRIPT_HANUNOO
UCDN_SCRIPT_BUHID = _fitz_old.UCDN_SCRIPT_BUHID
UCDN_SCRIPT_TAGBANWA = _fitz_old.UCDN_SCRIPT_TAGBANWA
UCDN_SCRIPT_LIMBU = _fitz_old.UCDN_SCRIPT_LIMBU
UCDN_SCRIPT_TAI_LE = _fitz_old.UCDN_SCRIPT_TAI_LE
UCDN_SCRIPT_LINEAR_B = _fitz_old.UCDN_SCRIPT_LINEAR_B
UCDN_SCRIPT_UGARITIC = _fitz_old.UCDN_SCRIPT_UGARITIC
UCDN_SCRIPT_SHAVIAN = _fitz_old.UCDN_SCRIPT_SHAVIAN
UCDN_SCRIPT_OSMANYA = _fitz_old.UCDN_SCRIPT_OSMANYA
UCDN_SCRIPT_CYPRIOT = _fitz_old.UCDN_SCRIPT_CYPRIOT
UCDN_SCRIPT_BRAILLE = _fitz_old.UCDN_SCRIPT_BRAILLE
UCDN_SCRIPT_BUGINESE = _fitz_old.UCDN_SCRIPT_BUGINESE
UCDN_SCRIPT_COPTIC = _fitz_old.UCDN_SCRIPT_COPTIC
UCDN_SCRIPT_NEW_TAI_LUE = _fitz_old.UCDN_SCRIPT_NEW_TAI_LUE
UCDN_SCRIPT_GLAGOLITIC = _fitz_old.UCDN_SCRIPT_GLAGOLITIC
UCDN_SCRIPT_TIFINAGH = _fitz_old.UCDN_SCRIPT_TIFINAGH
UCDN_SCRIPT_SYLOTI_NAGRI = _fitz_old.UCDN_SCRIPT_SYLOTI_NAGRI
UCDN_SCRIPT_OLD_PERSIAN = _fitz_old.UCDN_SCRIPT_OLD_PERSIAN
UCDN_SCRIPT_KHAROSHTHI = _fitz_old.UCDN_SCRIPT_KHAROSHTHI
UCDN_SCRIPT_BALINESE = _fitz_old.UCDN_SCRIPT_BALINESE
UCDN_SCRIPT_CUNEIFORM = _fitz_old.UCDN_SCRIPT_CUNEIFORM
UCDN_SCRIPT_PHOENICIAN = _fitz_old.UCDN_SCRIPT_PHOENICIAN
UCDN_SCRIPT_PHAGS_PA = _fitz_old.UCDN_SCRIPT_PHAGS_PA
UCDN_SCRIPT_NKO = _fitz_old.UCDN_SCRIPT_NKO
UCDN_SCRIPT_SUNDANESE = _fitz_old.UCDN_SCRIPT_SUNDANESE
UCDN_SCRIPT_LEPCHA = _fitz_old.UCDN_SCRIPT_LEPCHA
UCDN_SCRIPT_OL_CHIKI = _fitz_old.UCDN_SCRIPT_OL_CHIKI
UCDN_SCRIPT_VAI = _fitz_old.UCDN_SCRIPT_VAI
UCDN_SCRIPT_SAURASHTRA = _fitz_old.UCDN_SCRIPT_SAURASHTRA
UCDN_SCRIPT_KAYAH_LI = _fitz_old.UCDN_SCRIPT_KAYAH_LI
UCDN_SCRIPT_REJANG = _fitz_old.UCDN_SCRIPT_REJANG
UCDN_SCRIPT_LYCIAN = _fitz_old.UCDN_SCRIPT_LYCIAN
UCDN_SCRIPT_CARIAN = _fitz_old.UCDN_SCRIPT_CARIAN
UCDN_SCRIPT_LYDIAN = _fitz_old.UCDN_SCRIPT_LYDIAN
UCDN_SCRIPT_CHAM = _fitz_old.UCDN_SCRIPT_CHAM
UCDN_SCRIPT_TAI_THAM = _fitz_old.UCDN_SCRIPT_TAI_THAM
UCDN_SCRIPT_TAI_VIET = _fitz_old.UCDN_SCRIPT_TAI_VIET
UCDN_SCRIPT_AVESTAN = _fitz_old.UCDN_SCRIPT_AVESTAN
UCDN_SCRIPT_EGYPTIAN_HIEROGLYPHS = _fitz_old.UCDN_SCRIPT_EGYPTIAN_HIEROGLYPHS
UCDN_SCRIPT_SAMARITAN = _fitz_old.UCDN_SCRIPT_SAMARITAN
UCDN_SCRIPT_LISU = _fitz_old.UCDN_SCRIPT_LISU
UCDN_SCRIPT_BAMUM = _fitz_old.UCDN_SCRIPT_BAMUM
UCDN_SCRIPT_JAVANESE = _fitz_old.UCDN_SCRIPT_JAVANESE
UCDN_SCRIPT_MEETEI_MAYEK = _fitz_old.UCDN_SCRIPT_MEETEI_MAYEK
UCDN_SCRIPT_IMPERIAL_ARAMAIC = _fitz_old.UCDN_SCRIPT_IMPERIAL_ARAMAIC
UCDN_SCRIPT_OLD_SOUTH_ARABIAN = _fitz_old.UCDN_SCRIPT_OLD_SOUTH_ARABIAN
UCDN_SCRIPT_INSCRIPTIONAL_PARTHIAN = _fitz_old.UCDN_SCRIPT_INSCRIPTIONAL_PARTHIAN
UCDN_SCRIPT_INSCRIPTIONAL_PAHLAVI = _fitz_old.UCDN_SCRIPT_INSCRIPTIONAL_PAHLAVI
UCDN_SCRIPT_OLD_TURKIC = _fitz_old.UCDN_SCRIPT_OLD_TURKIC
UCDN_SCRIPT_KAITHI = _fitz_old.UCDN_SCRIPT_KAITHI
UCDN_SCRIPT_BATAK = _fitz_old.UCDN_SCRIPT_BATAK
UCDN_SCRIPT_BRAHMI = _fitz_old.UCDN_SCRIPT_BRAHMI
UCDN_SCRIPT_MANDAIC = _fitz_old.UCDN_SCRIPT_MANDAIC
UCDN_SCRIPT_CHAKMA = _fitz_old.UCDN_SCRIPT_CHAKMA
UCDN_SCRIPT_MEROITIC_CURSIVE = _fitz_old.UCDN_SCRIPT_MEROITIC_CURSIVE
UCDN_SCRIPT_MEROITIC_HIEROGLYPHS = _fitz_old.UCDN_SCRIPT_MEROITIC_HIEROGLYPHS
UCDN_SCRIPT_MIAO = _fitz_old.UCDN_SCRIPT_MIAO
UCDN_SCRIPT_SHARADA = _fitz_old.UCDN_SCRIPT_SHARADA
UCDN_SCRIPT_SORA_SOMPENG = _fitz_old.UCDN_SCRIPT_SORA_SOMPENG
UCDN_SCRIPT_TAKRI = _fitz_old.UCDN_SCRIPT_TAKRI
UCDN_SCRIPT_UNKNOWN = _fitz_old.UCDN_SCRIPT_UNKNOWN
UCDN_SCRIPT_BASSA_VAH = _fitz_old.UCDN_SCRIPT_BASSA_VAH
UCDN_SCRIPT_CAUCASIAN_ALBANIAN = _fitz_old.UCDN_SCRIPT_CAUCASIAN_ALBANIAN
UCDN_SCRIPT_DUPLOYAN = _fitz_old.UCDN_SCRIPT_DUPLOYAN
UCDN_SCRIPT_ELBASAN = _fitz_old.UCDN_SCRIPT_ELBASAN
UCDN_SCRIPT_GRANTHA = _fitz_old.UCDN_SCRIPT_GRANTHA
UCDN_SCRIPT_KHOJKI = _fitz_old.UCDN_SCRIPT_KHOJKI
UCDN_SCRIPT_KHUDAWADI = _fitz_old.UCDN_SCRIPT_KHUDAWADI
UCDN_SCRIPT_LINEAR_A = _fitz_old.UCDN_SCRIPT_LINEAR_A
UCDN_SCRIPT_MAHAJANI = _fitz_old.UCDN_SCRIPT_MAHAJANI
UCDN_SCRIPT_MANICHAEAN = _fitz_old.UCDN_SCRIPT_MANICHAEAN
UCDN_SCRIPT_MENDE_KIKAKUI = _fitz_old.UCDN_SCRIPT_MENDE_KIKAKUI
UCDN_SCRIPT_MODI = _fitz_old.UCDN_SCRIPT_MODI
UCDN_SCRIPT_MRO = _fitz_old.UCDN_SCRIPT_MRO
UCDN_SCRIPT_NABATAEAN = _fitz_old.UCDN_SCRIPT_NABATAEAN
UCDN_SCRIPT_OLD_NORTH_ARABIAN = _fitz_old.UCDN_SCRIPT_OLD_NORTH_ARABIAN
UCDN_SCRIPT_OLD_PERMIC = _fitz_old.UCDN_SCRIPT_OLD_PERMIC
UCDN_SCRIPT_PAHAWH_HMONG = _fitz_old.UCDN_SCRIPT_PAHAWH_HMONG
UCDN_SCRIPT_PALMYRENE = _fitz_old.UCDN_SCRIPT_PALMYRENE
UCDN_SCRIPT_PAU_CIN_HAU = _fitz_old.UCDN_SCRIPT_PAU_CIN_HAU
UCDN_SCRIPT_PSALTER_PAHLAVI = _fitz_old.UCDN_SCRIPT_PSALTER_PAHLAVI
UCDN_SCRIPT_SIDDHAM = _fitz_old.UCDN_SCRIPT_SIDDHAM
UCDN_SCRIPT_TIRHUTA = _fitz_old.UCDN_SCRIPT_TIRHUTA
UCDN_SCRIPT_WARANG_CITI = _fitz_old.UCDN_SCRIPT_WARANG_CITI
UCDN_SCRIPT_AHOM = _fitz_old.UCDN_SCRIPT_AHOM
UCDN_SCRIPT_ANATOLIAN_HIEROGLYPHS = _fitz_old.UCDN_SCRIPT_ANATOLIAN_HIEROGLYPHS
UCDN_SCRIPT_HATRAN = _fitz_old.UCDN_SCRIPT_HATRAN
UCDN_SCRIPT_MULTANI = _fitz_old.UCDN_SCRIPT_MULTANI
UCDN_SCRIPT_OLD_HUNGARIAN = _fitz_old.UCDN_SCRIPT_OLD_HUNGARIAN
UCDN_SCRIPT_SIGNWRITING = _fitz_old.UCDN_SCRIPT_SIGNWRITING
UCDN_SCRIPT_ADLAM = _fitz_old.UCDN_SCRIPT_ADLAM
UCDN_SCRIPT_BHAIKSUKI = _fitz_old.UCDN_SCRIPT_BHAIKSUKI
UCDN_SCRIPT_MARCHEN = _fitz_old.UCDN_SCRIPT_MARCHEN
UCDN_SCRIPT_NEWA = _fitz_old.UCDN_SCRIPT_NEWA
UCDN_SCRIPT_OSAGE = _fitz_old.UCDN_SCRIPT_OSAGE
UCDN_SCRIPT_TANGUT = _fitz_old.UCDN_SCRIPT_TANGUT
UCDN_SCRIPT_MASARAM_GONDI = _fitz_old.UCDN_SCRIPT_MASARAM_GONDI
UCDN_SCRIPT_NUSHU = _fitz_old.UCDN_SCRIPT_NUSHU
UCDN_SCRIPT_SOYOMBO = _fitz_old.UCDN_SCRIPT_SOYOMBO
UCDN_SCRIPT_ZANABAZAR_SQUARE = _fitz_old.UCDN_SCRIPT_ZANABAZAR_SQUARE
UCDN_SCRIPT_DOGRA = _fitz_old.UCDN_SCRIPT_DOGRA
UCDN_SCRIPT_GUNJALA_GONDI = _fitz_old.UCDN_SCRIPT_GUNJALA_GONDI
UCDN_SCRIPT_HANIFI_ROHINGYA = _fitz_old.UCDN_SCRIPT_HANIFI_ROHINGYA
UCDN_SCRIPT_MAKASAR = _fitz_old.UCDN_SCRIPT_MAKASAR
UCDN_SCRIPT_MEDEFAIDRIN = _fitz_old.UCDN_SCRIPT_MEDEFAIDRIN
UCDN_SCRIPT_OLD_SOGDIAN = _fitz_old.UCDN_SCRIPT_OLD_SOGDIAN
UCDN_SCRIPT_SOGDIAN = _fitz_old.UCDN_SCRIPT_SOGDIAN
UCDN_SCRIPT_ELYMAIC = _fitz_old.UCDN_SCRIPT_ELYMAIC
UCDN_SCRIPT_NANDINAGARI = _fitz_old.UCDN_SCRIPT_NANDINAGARI
UCDN_SCRIPT_NYIAKENG_PUACHUE_HMONG = _fitz_old.UCDN_SCRIPT_NYIAKENG_PUACHUE_HMONG
UCDN_SCRIPT_WANCHO = _fitz_old.UCDN_SCRIPT_WANCHO
def _set_FileDataError(value):
return _fitz_old._set_FileDataError(value)
def util_sine_between(C, P, Q):
return _fitz_old.util_sine_between(C, P, Q)
def util_hor_matrix(C, P):
return _fitz_old.util_hor_matrix(C, P)
def util_ensure_widget_calc(annot):
return _fitz_old.util_ensure_widget_calc(annot)
def util_make_rect(a):
return _fitz_old.util_make_rect(a)
def util_make_irect(a):
return _fitz_old.util_make_irect(a)
def util_round_rect(rect):
return _fitz_old.util_round_rect(rect)
def util_transform_rect(rect, matrix):
return _fitz_old.util_transform_rect(rect, matrix)
def util_intersect_rect(r1, r2):
return _fitz_old.util_intersect_rect(r1, r2)
def util_is_point_in_rect(p, r):
return _fitz_old.util_is_point_in_rect(p, r)
def util_include_point_in_rect(r, p):
return _fitz_old.util_include_point_in_rect(r, p)
def util_point_in_quad(P, Q):
return _fitz_old.util_point_in_quad(P, Q)
def util_transform_point(point, matrix):
return _fitz_old.util_transform_point(point, matrix)
def util_union_rect(r1, r2):
return _fitz_old.util_union_rect(r1, r2)
def util_concat_matrix(m1, m2):
return _fitz_old.util_concat_matrix(m1, m2)
def util_invert_matrix(matrix):
return _fitz_old.util_invert_matrix(matrix)
def util_measure_string(text, fontname, fontsize, encoding):
return _fitz_old.util_measure_string(text, fontname, fontsize, encoding)
# ------------------------------------------------------------------------
# Copyright 2020-2022, Harald Lieder, mailto:harald.lieder@outlook.com
# License: GNU AFFERO GPL 3.0, https://www.gnu.org/licenses/agpl-3.0.html
#
# Part of "PyMuPDF", a Python binding for "MuPDF" (http://mupdf.com), a
# lightweight PDF, XPS, and E-book viewer, renderer and toolkit which is
# maintained and developed by Artifex Software, Inc. https://artifex.com.
# ------------------------------------------------------------------------
# largest 32bit integers surviving C float conversion roundtrips
# used by MuPDF to define infinite rectangles
FZ_MIN_INF_RECT = -0x80000000
FZ_MAX_INF_RECT = 0x7fffff80
class Matrix(object):
"""Matrix() - all zeros
Matrix(a, b, c, d, e, f)
Matrix(zoom-x, zoom-y) - zoom
Matrix(shear-x, shear-y, 1) - shear
Matrix(degree) - rotate
Matrix(Matrix) - new copy
Matrix(sequence) - from 'sequence'"""
def __init__(self, *args):
if not args:
self.a = self.b = self.c = self.d = self.e = self.f = 0.0
return None
if len(args) > 6:
raise ValueError("Matrix: bad seq len")
if len(args) == 6: # 6 numbers
self.a, self.b, self.c, self.d, self.e, self.f = map(float, args)
return None
if len(args) == 1: # either an angle or a sequ
if hasattr(args[0], "__float__"):
theta = math.radians(args[0])
c = round(math.cos(theta), 12)
s = round(math.sin(theta), 12)
self.a = self.d = c
self.b = s
self.c = -s
self.e = self.f = 0.0
return None
else:
self.a, self.b, self.c, self.d, self.e, self.f = map(float, args[0])
return None
if len(args) == 2 or len(args) == 3 and args[2] == 0:
self.a, self.b, self.c, self.d, self.e, self.f = float(args[0]), \
0.0, 0.0, float(args[1]), 0.0, 0.0
return None
if len(args) == 3 and args[2] == 1:
self.a, self.b, self.c, self.d, self.e, self.f = 1.0, \
float(args[1]), float(args[0]), 1.0, 0.0, 0.0
return None
raise ValueError("Matrix: bad args")
def invert(self, src=None):
"""Calculate the inverted matrix. Return 0 if successful and replace
current one. Else return 1 and do nothing.
"""
if src is None:
dst = util_invert_matrix(self)
else:
dst = util_invert_matrix(src)
if dst[0] == 1:
return 1
self.a, self.b, self.c, self.d, self.e, self.f = dst[1]
return 0
def pretranslate(self, tx, ty):
"""Calculate pre translation and replace current matrix."""
tx = float(tx)
ty = float(ty)
self.e += tx * self.a + ty * self.c
self.f += tx * self.b + ty * self.d
return self
def prescale(self, sx, sy):
"""Calculate pre scaling and replace current matrix."""
sx = float(sx)
sy = float(sy)
self.a *= sx
self.b *= sx
self.c *= sy
self.d *= sy
return self
def preshear(self, h, v):
"""Calculate pre shearing and replace current matrix."""
h = float(h)
v = float(v)
a, b = self.a, self.b
self.a += v * self.c
self.b += v * self.d
self.c += h * a
self.d += h * b
return self
def prerotate(self, theta):
"""Calculate pre rotation and replace current matrix."""
theta = float(theta)
while theta < 0: theta += 360
while theta >= 360: theta -= 360
if abs(0 - theta) < EPSILON:
pass
elif abs(90.0 - theta) < EPSILON:
a = self.a
b = self.b
self.a = self.c
self.b = self.d
self.c = -a
self.d = -b
elif abs(180.0 - theta) < EPSILON:
self.a = -self.a
self.b = -self.b
self.c = -self.c
self.d = -self.d
elif abs(270.0 - theta) < EPSILON:
a = self.a
b = self.b
self.a = -self.c
self.b = -self.d
self.c = a
self.d = b
else:
rad = math.radians(theta)
s = math.sin(rad)
c = math.cos(rad)
a = self.a
b = self.b
self.a = c * a + s * self.c
self.b = c * b + s * self.d
self.c =-s * a + c * self.c
self.d =-s * b + c * self.d
return self
def concat(self, one, two):
"""Multiply two matrices and replace current one."""
if not len(one) == len(two) == 6:
raise ValueError("Matrix: bad seq len")
self.a, self.b, self.c, self.d, self.e, self.f = util_concat_matrix(one, two)
return self
def __getitem__(self, i):
return (self.a, self.b, self.c, self.d, self.e, self.f)[i]
def __setitem__(self, i, v):
v = float(v)
if i == 0: self.a = v
elif i == 1: self.b = v
elif i == 2: self.c = v
elif i == 3: self.d = v
elif i == 4: self.e = v
elif i == 5: self.f = v
else:
raise IndexError("index out of range")
return
def __len__(self):
return 6
def __repr__(self):
return "Matrix" + str(tuple(self))
def __invert__(self):
"""Calculate inverted matrix."""
m1 = Matrix()
m1.invert(self)
return m1
__inv__ = __invert__
def __mul__(self, m):
if hasattr(m, "__float__"):
return Matrix(self.a * m, self.b * m, self.c * m,
self.d * m, self.e * m, self.f * m)
m1 = Matrix(1,1)
return m1.concat(self, m)
def __truediv__(self, m):
if hasattr(m, "__float__"):
return Matrix(self.a * 1./m, self.b * 1./m, self.c * 1./m,
self.d * 1./m, self.e * 1./m, self.f * 1./m)
m1 = util_invert_matrix(m)[1]
if not m1:
raise ZeroDivisionError("matrix not invertible")
m2 = Matrix(1,1)
return m2.concat(self, m1)
__div__ = __truediv__
def __add__(self, m):
if hasattr(m, "__float__"):
return Matrix(self.a + m, self.b + m, self.c + m,
self.d + m, self.e + m, self.f + m)
if len(m) != 6:
raise ValueError("Matrix: bad seq len")
return Matrix(self.a + m[0], self.b + m[1], self.c + m[2],
self.d + m[3], self.e + m[4], self.f + m[5])
def __sub__(self, m):
if hasattr(m, "__float__"):
return Matrix(self.a - m, self.b - m, self.c - m,
self.d - m, self.e - m, self.f - m)
if len(m) != 6:
raise ValueError("Matrix: bad seq len")
return Matrix(self.a - m[0], self.b - m[1], self.c - m[2],
self.d - m[3], self.e - m[4], self.f - m[5])
def __pos__(self):
return Matrix(self)
def __neg__(self):
return Matrix(-self.a, -self.b, -self.c, -self.d, -self.e, -self.f)
def __bool__(self):
return not (max(self) == min(self) == 0)
def __nonzero__(self):
return not (max(self) == min(self) == 0)
def __eq__(self, mat):
if not hasattr(mat, "__len__"):
return False
return len(mat) == 6 and bool(self - mat) is False
def __abs__(self):
return math.sqrt(sum([c*c for c in self]))
norm = __abs__
@property
def is_rectilinear(self):
"""True if rectangles are mapped to rectangles."""
return (abs(self.b) < EPSILON and abs(self.c) < EPSILON) or \
(abs(self.a) < EPSILON and abs(self.d) < EPSILON);
class IdentityMatrix(Matrix):
"""Identity matrix [1, 0, 0, 1, 0, 0]"""
def __init__(self):
Matrix.__init__(self, 1.0, 1.0)
def __setattr__(self, name, value):
if name in "ad":
self.__dict__[name] = 1.0
elif name in "bcef":
self.__dict__[name] = 0.0
else:
self.__dict__[name] = value
def checkargs(*args):
raise NotImplementedError("Identity is readonly")
prerotate = checkargs
preshear = checkargs
prescale = checkargs
pretranslate = checkargs
concat = checkargs
invert = checkargs
def __repr__(self):
return "IdentityMatrix(1.0, 0.0, 0.0, 1.0, 0.0, 0.0)"
def __hash__(self):
return hash((1,0,0,1,0,0))
Identity = IdentityMatrix()
class Point(object):
"""Point() - all zeros\nPoint(x, y)\nPoint(Point) - new copy\nPoint(sequence) - from 'sequence'"""
def __init__(self, *args):
if not args:
self.x = 0.0
self.y = 0.0
return None
if len(args) > 2:
raise ValueError("Point: bad seq len")
if len(args) == 2:
self.x = float(args[0])
self.y = float(args[1])
return None
if len(args) == 1:
l = args[0]
if hasattr(l, "__getitem__") is False:
raise ValueError("Point: bad args")
if len(l) != 2:
raise ValueError("Point: bad seq len")
self.x = float(l[0])
self.y = float(l[1])
return None
raise ValueError("Point: bad args")
def transform(self, m):
"""Replace point by its transformation with matrix-like m."""
if len(m) != 6:
raise ValueError("Matrix: bad seq len")
self.x, self.y = util_transform_point(self, m)
return self
@property
def unit(self):
"""Unit vector of the point."""
s = self.x * self.x + self.y * self.y
if s < EPSILON:
return Point(0,0)
s = math.sqrt(s)
return Point(self.x / s, self.y / s)
@property
def abs_unit(self):
"""Unit vector with positive coordinates."""
s = self.x * self.x + self.y * self.y
if s < EPSILON:
return Point(0,0)
s = math.sqrt(s)
return Point(abs(self.x) / s, abs(self.y) / s)
def distance_to(self, *args):
"""Return distance to rectangle or another point."""
if not len(args) > 0:
raise ValueError("at least one parameter must be given")
x = args[0]
if len(x) == 2:
x = Point(x)
elif len(x) == 4:
x = Rect(x)
else:
raise ValueError("arg1 must be point-like or rect-like")
if len(args) > 1:
unit = args[1]
else:
unit = "px"
u = {"px": (1.,1.), "in": (1.,72.), "cm": (2.54, 72.),
"mm": (25.4, 72.)}
f = u[unit][0] / u[unit][1]
if type(x) is Point:
return abs(self - x) * f
# from here on, x is a rectangle
# as a safeguard, make a finite copy of it
r = Rect(x.top_left, x.top_left)
r = r | x.bottom_right
if self in r:
return 0.0
if self.x > r.x1:
if self.y >= r.y1:
return self.distance_to(r.bottom_right, unit)
elif self.y <= r.y0:
return self.distance_to(r.top_right, unit)
else:
return (self.x - r.x1) * f
elif r.x0 <= self.x <= r.x1:
if self.y >= r.y1:
return (self.y - r.y1) * f
else:
return (r.y0 - self.y) * f
else:
if self.y >= r.y1:
return self.distance_to(r.bottom_left, unit)
elif self.y <= r.y0:
return self.distance_to(r.top_left, unit)
else:
return (r.x0 - self.x) * f
def __getitem__(self, i):
return (self.x, self.y)[i]
def __len__(self):
return 2
def __setitem__(self, i, v):
v = float(v)
if i == 0: self.x = v
elif i == 1: self.y = v
else:
raise IndexError("index out of range")
return None
def __repr__(self):
return "Point" + str(tuple(self))
def __pos__(self):
return Point(self)
def __neg__(self):
return Point(-self.x, -self.y)
def __bool__(self):
return not (max(self) == min(self) == 0)
def __nonzero__(self):
return not (max(self) == min(self) == 0)
def __eq__(self, p):
if not hasattr(p, "__len__"):
return False
return len(p) == 2 and bool(self - p) is False
def __abs__(self):
return math.sqrt(self.x * self.x + self.y * self.y)
norm = __abs__
def __add__(self, p):
if hasattr(p, "__float__"):
return Point(self.x + p, self.y + p)
if len(p) != 2:
raise ValueError("Point: bad seq len")
return Point(self.x + p[0], self.y + p[1])
def __sub__(self, p):
if hasattr(p, "__float__"):
return Point(self.x - p, self.y - p)
if len(p) != 2:
raise ValueError("Point: bad seq len")
return Point(self.x - p[0], self.y - p[1])
def __mul__(self, m):
if hasattr(m, "__float__"):
return Point(self.x * m, self.y * m)
p = Point(self)
return p.transform(m)
def __truediv__(self, m):
if hasattr(m, "__float__"):
return Point(self.x * 1./m, self.y * 1./m)
m1 = util_invert_matrix(m)[1]
if not m1:
raise ZeroDivisionError("matrix not invertible")
p = Point(self)
return p.transform(m1)
__div__ = __truediv__
def __hash__(self):
return hash(tuple(self))
class Rect(object):
"""Rect() - all zeros
Rect(x0, y0, x1, y1) - 4 coordinates
Rect(top-left, x1, y1) - point and 2 coordinates
Rect(x0, y0, bottom-right) - 2 coordinates and point
Rect(top-left, bottom-right) - 2 points
Rect(sequ) - new from sequence or rect-like
"""
def __init__(self, *args):
self.x0, self.y0, self.x1, self.y1 = util_make_rect(args)
return None
def normalize(self):
"""Replace rectangle with its valid version."""
if self.x1 < self.x0:
self.x0, self.x1 = self.x1, self.x0
if self.y1 < self.y0:
self.y0, self.y1 = self.y1, self.y0
return self
@property
def is_empty(self):
"""True if rectangle area is empty."""
return self.x0 >= self.x1 or self.y0 >= self.y1
@property
def is_valid(self):
"""True if rectangle is valid."""
return self.x0 <= self.x1 and self.y0 <= self.y1
@property
def is_infinite(self):
"""True if this is the infinite rectangle."""
return self.x0 == self.y0 == FZ_MIN_INF_RECT and self.x1 == self.y1 == FZ_MAX_INF_RECT
@property
def top_left(self):
"""Top-left corner."""
return Point(self.x0, self.y0)
@property
def top_right(self):
"""Top-right corner."""
return Point(self.x1, self.y0)
@property
def bottom_left(self):
"""Bottom-left corner."""
return Point(self.x0, self.y1)
@property
def bottom_right(self):
"""Bottom-right corner."""
return Point(self.x1, self.y1)
tl = top_left
tr = top_right
bl = bottom_left
br = bottom_right
@property
def quad(self):
"""Return Quad version of rectangle."""
return Quad(self.tl, self.tr, self.bl, self.br)
def torect(self, r):
"""Return matrix that converts to target rect."""
r = Rect(r)
if self.is_infinite or self.is_empty or r.is_infinite or r.is_empty:
raise ValueError("rectangles must be finite and not empty")
return (
Matrix(1, 0, 0, 1, -self.x0, -self.y0)
* Matrix(r.width / self.width, r.height / self.height)
* Matrix(1, 0, 0, 1, r.x0, r.y0)
)
def morph(self, p, m):
"""Morph with matrix-like m and point-like p.
Returns a new quad."""
if self.is_infinite:
return INFINITE_QUAD()
return self.quad.morph(p, m)
def round(self):
"""Return the IRect."""
return IRect(util_round_rect(self))
irect = property(round)
width = property(lambda self: self.x1 - self.x0 if self.x1 > self.x0 else 0)
height = property(lambda self: self.y1 - self.y0 if self.y1 > self.y0 else 0)
def include_point(self, p):
"""Extend to include point-like p."""
if len(p) != 2:
raise ValueError("Point: bad seq len")
self.x0, self.y0, self.x1, self.y1 = util_include_point_in_rect(self, p)
return self
def include_rect(self, r):
"""Extend to include rect-like r."""
if len(r) != 4:
raise ValueError("Rect: bad seq len")
r = Rect(r)
if r.is_infinite or self.is_infinite:
self.x0, self.y0, self.x1, self.y1 = FZ_MIN_INF_RECT, FZ_MIN_INF_RECT, FZ_MAX_INF_RECT, FZ_MAX_INF_RECT
elif r.is_empty:
return self
elif self.is_empty:
self.x0, self.y0, self.x1, self.y1 = r.x0, r.y0, r.x1, r.y1
else:
self.x0, self.y0, self.x1, self.y1 = util_union_rect(self, r)
return self
def intersect(self, r):
"""Restrict to common rect with rect-like r."""
if not len(r) == 4:
raise ValueError("Rect: bad seq len")
r = Rect(r)
if r.is_infinite:
return self
elif self.is_infinite:
self.x0, self.y0, self.x1, self.y1 = r.x0, r.y0, r.x1, r.y1
elif r.is_empty:
self.x0, self.y0, self.x1, self.y1 = r.x0, r.y0, r.x1, r.y1
elif self.is_empty:
return self
else:
self.x0, self.y0, self.x1, self.y1 = util_intersect_rect(self, r)
return self
def contains(self, x):
"""Check if containing point-like or rect-like x."""
return self.__contains__(x)
def transform(self, m):
"""Replace with the transformation by matrix-like m."""
if not len(m) == 6:
raise ValueError("Matrix: bad seq len")
self.x0, self.y0, self.x1, self.y1 = util_transform_rect(self, m)
return self
def __getitem__(self, i):
return (self.x0, self.y0, self.x1, self.y1)[i]
def __len__(self):
return 4
def __setitem__(self, i, v):
v = float(v)
if i == 0: self.x0 = v
elif i == 1: self.y0 = v
elif i == 2: self.x1 = v
elif i == 3: self.y1 = v
else:
raise IndexError("index out of range")
return None
def __repr__(self):
return "Rect" + str(tuple(self))
def __pos__(self):
return Rect(self)
def __neg__(self):
return Rect(-self.x0, -self.y0, -self.x1, -self.y1)
def __bool__(self):
return not self.x0 == self.y0 == self.x1 == self.y1 == 0
def __nonzero__(self):
return not self.x0 == self.y0 == self.x1 == self.y1 == 0
def __eq__(self, r):
if not hasattr(r, "__len__"):
return False
return len(r) == 4 and self.x0 == r[0] and self.y0 == r[1] and self.x1 == r[2] and self.y1 == r[3]
def __abs__(self):
if self.is_infinite or not self.is_valid:
return 0.0
return self.width * self.height
def norm(self):
return math.sqrt(sum([c*c for c in self]))
def __add__(self, p):
if hasattr(p, "__float__"):
return Rect(self.x0 + p, self.y0 + p, self.x1 + p, self.y1 + p)
if len(p) != 4:
raise ValueError("Rect: bad seq len")
return Rect(self.x0 + p[0], self.y0 + p[1], self.x1 + p[2], self.y1 + p[3])
def __sub__(self, p):
if hasattr(p, "__float__"):
return Rect(self.x0 - p, self.y0 - p, self.x1 - p, self.y1 - p)
if len(p) != 4:
raise ValueError("Rect: bad seq len")
return Rect(self.x0 - p[0], self.y0 - p[1], self.x1 - p[2], self.y1 - p[3])
def __mul__(self, m):
if hasattr(m, "__float__"):
return Rect(self.x0 * m, self.y0 * m, self.x1 * m, self.y1 * m)
r = Rect(self)
r = r.transform(m)
return r
def __truediv__(self, m):
if hasattr(m, "__float__"):
return Rect(self.x0 * 1./m, self.y0 * 1./m, self.x1 * 1./m, self.y1 * 1./m)
im = util_invert_matrix(m)[1]
if not im:
raise ZeroDivisionError("Matrix not invertible")
r = Rect(self)
r = r.transform(im)
return r
__div__ = __truediv__
def __contains__(self, x):
if hasattr(x, "__float__"):
return x in tuple(self)
l = len(x)
if l == 2:
return util_is_point_in_rect(x, self)
if l == 4:
r = INFINITE_RECT()
try:
r = Rect(x)
except:
r = Quad(x).rect
return (self.x0 <= r.x0 <= r.x1 <= self.x1 and
self.y0 <= r.y0 <= r.y1 <= self.y1)
return False
def __or__(self, x):
if not hasattr(x, "__len__"):
raise ValueError("bad type op 2")
r = Rect(self)
if len(x) == 2:
return r.include_point(x)
if len(x) == 4:
return r.include_rect(x)
raise ValueError("bad type op 2")
def __and__(self, x):
if not hasattr(x, "__len__") or len(x) != 4:
raise ValueError("bad type op 2")
r = Rect(self)
return r.intersect(x)
def intersects(self, x):
"""Check if intersection with rectangle x is not empty."""
r1 = Rect(x)
if self.is_empty or self.is_infinite or r1.is_empty or r1.is_infinite:
return False
r = Rect(self)
if r.intersect(r1).is_empty:
return False
return True
def __hash__(self):
return hash(tuple(self))
class IRect(object):
"""IRect() - all zeros
IRect(x0, y0, x1, y1) - 4 coordinates
IRect(top-left, x1, y1) - point and 2 coordinates
IRect(x0, y0, bottom-right) - 2 coordinates and point
IRect(top-left, bottom-right) - 2 points
IRect(sequ) - new from sequence or rect-like
"""
def __init__(self, *args):
self.x0, self.y0, self.x1, self.y1 = util_make_irect(args)
return None
def normalize(self):
"""Replace rectangle with its valid version."""
if self.x1 < self.x0:
self.x0, self.x1 = self.x1, self.x0
if self.y1 < self.y0:
self.y0, self.y1 = self.y1, self.y0
return self
@property
def is_empty(self):
"""True if rectangle area is empty."""
return self.x0 >= self.x1 or self.y0 >= self.y1
@property
def is_valid(self):
"""True if rectangle is valid."""
return self.x0 <= self.x1 and self.y0 <= self.y1
@property
def is_infinite(self):
"""True if rectangle is infinite."""
return self.x0 == self.y0 == FZ_MIN_INF_RECT and self.x1 == self.y1 == FZ_MAX_INF_RECT
@property
def top_left(self):
"""Top-left corner."""
return Point(self.x0, self.y0)
@property
def top_right(self):
"""Top-right corner."""
return Point(self.x1, self.y0)
@property
def bottom_left(self):
"""Bottom-left corner."""
return Point(self.x0, self.y1)
@property
def bottom_right(self):
"""Bottom-right corner."""
return Point(self.x1, self.y1)
tl = top_left
tr = top_right
bl = bottom_left
br = bottom_right
@property
def quad(self):
"""Return Quad version of rectangle."""
return Quad(self.tl, self.tr, self.bl, self.br)
def torect(self, r):
"""Return matrix that converts to target rect."""
r = Rect(r)
if self.is_infinite or self.is_empty or r.is_infinite or r.is_empty:
raise ValueError("rectangles must be finite and not empty")
return (
Matrix(1, 0, 0, 1, -self.x0, -self.y0)
* Matrix(r.width / self.width, r.height / self.height)
* Matrix(1, 0, 0, 1, r.x0, r.y0)
)
def morph(self, p, m):
"""Morph with matrix-like m and point-like p.
Returns a new quad."""
if self.is_infinite:
return INFINITE_QUAD()
return self.quad.morph(p, m)
@property
def rect(self):
return Rect(self)
width = property(lambda self: self.x1 - self.x0 if self.x1 > self.x0 else 0)
height = property(lambda self: self.y1 - self.y0 if self.y1 > self.y0 else 0)
def include_point(self, p):
"""Extend rectangle to include point p."""
rect = self.rect.include_point(p)
return rect.irect
def include_rect(self, r):
"""Extend rectangle to include rectangle r."""
rect = self.rect.include_rect(r)
return rect.irect
def intersect(self, r):
"""Restrict rectangle to intersection with rectangle r."""
rect = self.rect.intersect(r)
return rect.irect
def __getitem__(self, i):
return (self.x0, self.y0, self.x1, self.y1)[i]
def __len__(self):
return 4
def __setitem__(self, i, v):
v = int(v)
if i == 0: self.x0 = v
elif i == 1: self.y0 = v
elif i == 2: self.x1 = v
elif i == 3: self.y1 = v
else:
raise IndexError("index out of range")
return None
def __repr__(self):
return "IRect" + str(tuple(self))
def __pos__(self):
return IRect(self)
def __neg__(self):
return IRect(-self.x0, -self.y0, -self.x1, -self.y1)
def __bool__(self):
return not self.x0 == self.y0 == self.x1 == self.y1 == 0
def __nonzero__(self):
return not self.x0 == self.y0 == self.x1 == self.y1 == 0
def __eq__(self, r):
if not hasattr(r, "__len__"):
return False
return len(r) == 4 and self.x0 == r[0] and self.y0 == r[1] and self.x1 == r[2] and self.y1 == r[3]
def __abs__(self):
if self.is_infinite or not self.is_valid:
return 0
return self.width * self.height
def norm(self):
return math.sqrt(sum([c*c for c in self]))
def __add__(self, p):
return Rect.__add__(self, p).round()
def __sub__(self, p):
return Rect.__sub__(self, p).round()
def transform(self, m):
return Rect.transform(self, m).round()
def __mul__(self, m):
return Rect.__mul__(self, m).round()
def __truediv__(self, m):
return Rect.__truediv__(self, m).round()
__div__ = __truediv__
def __contains__(self, x):
return Rect.__contains__(self, x)
def __or__(self, x):
return Rect.__or__(self, x).round()
def __and__(self, x):
return Rect.__and__(self, x).round()
def intersects(self, x):
return Rect.intersects(self, x)
def __hash__(self):
return hash(tuple(self))
class Quad(object):
"""Quad() - all zero points\nQuad(ul, ur, ll, lr)\nQuad(quad) - new copy\nQuad(sequence) - from 'sequence'"""
def __init__(self, *args):
if not args:
self.ul = self.ur = self.ll = self.lr = Point()
return None
if len(args) > 4:
raise ValueError("Quad: bad seq len")
if len(args) == 4:
self.ul, self.ur, self.ll, self.lr = map(Point, args)
return None
if len(args) == 1:
l = args[0]
if hasattr(l, "__getitem__") is False:
raise ValueError("Quad: bad args")
if len(l) != 4:
raise ValueError("Quad: bad seq len")
self.ul, self.ur, self.ll, self.lr = map(Point, l)
return None
raise ValueError("Quad: bad args")
@property
def is_rectangular(self)->bool:
"""Check if quad is rectangular.
Notes:
Some rotation matrix can thus transform it into a rectangle.
This is equivalent to three corners enclose 90 degrees.
Returns:
True or False.
"""
sine = util_sine_between(self.ul, self.ur, self.lr)
if abs(sine - 1) > EPSILON: # the sine of the angle
return False
sine = util_sine_between(self.ur, self.lr, self.ll)
if abs(sine - 1) > EPSILON:
return False
sine = util_sine_between(self.lr, self.ll, self.ul)
if abs(sine - 1) > EPSILON:
return False
return True
@property
def is_convex(self)->bool:
"""Check if quad is convex and not degenerate.
Notes:
Check that for the two diagonals, the other two corners are not
on the same side of the diagonal.
Returns:
True or False.
"""
m = planish_line(self.ul, self.lr) # puts this diagonal on x-axis
p1 = self.ll * m # transform the
p2 = self.ur * m # other two points
if p1.y * p2.y > 0:
return False
m = planish_line(self.ll, self.ur) # puts other diagonal on x-axis
p1 = self.lr * m # tranform the
p2 = self.ul * m # remaining points
if p1.y * p2.y > 0:
return False
return True
width = property(lambda self: max(abs(self.ul - self.ur), abs(self.ll - self.lr)))
height = property(lambda self: max(abs(self.ul - self.ll), abs(self.ur - self.lr)))
@property
def is_empty(self):
"""Check whether all quad corners are on the same line.
This is the case if width or height is zero.
"""
return self.width < EPSILON or self.height < EPSILON
@property
def is_infinite(self):
"""Check whether this is the infinite quad."""
return self.rect.is_infinite
@property
def rect(self):
r = Rect()
r.x0 = min(self.ul.x, self.ur.x, self.lr.x, self.ll.x)
r.y0 = min(self.ul.y, self.ur.y, self.lr.y, self.ll.y)
r.x1 = max(self.ul.x, self.ur.x, self.lr.x, self.ll.x)
r.y1 = max(self.ul.y, self.ur.y, self.lr.y, self.ll.y)
return r
def __contains__(self, x):
try:
l = x.__len__()
except:
return False
if l == 2:
return util_point_in_quad(x, self)
if l != 4:
return False
if CheckRect(x):
if Rect(x).is_empty:
return True
return util_point_in_quad(x[:2], self) and util_point_in_quad(x[2:], self)
if CheckQuad(x):
for i in range(4):
if not util_point_in_quad(x[i], self):
return False
return True
return False
def __getitem__(self, i):
return (self.ul, self.ur, self.ll, self.lr)[i]
def __len__(self):
return 4
def __setitem__(self, i, v):
if i == 0: self.ul = Point(v)
elif i == 1: self.ur = Point(v)
elif i == 2: self.ll = Point(v)
elif i == 3: self.lr = Point(v)
else:
raise IndexError("index out of range")
return None
def __repr__(self):
return "Quad" + str(tuple(self))
def __pos__(self):
return Quad(self)
def __neg__(self):
return Quad(-self.ul, -self.ur, -self.ll, -self.lr)
def __bool__(self):
return not self.is_empty
def __nonzero__(self):
return not self.is_empty
def __eq__(self, quad):
if not hasattr(quad, "__len__"):
return False
return len(quad) == 4 and (
self.ul == quad[0] and
self.ur == quad[1] and
self.ll == quad[2] and
self.lr == quad[3]
)
def __abs__(self):
if self.is_empty:
return 0.0
return abs(self.ul - self.ur) * abs(self.ul - self.ll)
def morph(self, p, m):
"""Morph the quad with matrix-like 'm' and point-like 'p'.
Return a new quad."""
if self.is_infinite:
return INFINITE_QUAD()
delta = Matrix(1, 1).pretranslate(p.x, p.y)
q = self * ~delta * m * delta
return q
def transform(self, m):
"""Replace quad by its transformation with matrix m."""
if hasattr(m, "__float__"):
pass
elif len(m) != 6:
raise ValueError("Matrix: bad seq len")
self.ul *= m
self.ur *= m
self.ll *= m
self.lr *= m
return self
def __mul__(self, m):
q = Quad(self)
q = q.transform(m)
return q
def __add__(self, q):
if hasattr(q, "__float__"):
return Quad(self.ul + q, self.ur + q, self.ll + q, self.lr + q)
if len(p) != 4:
raise ValueError("Quad: bad seq len")
return Quad(self.ul + q[0], self.ur + q[1], self.ll + q[2], self.lr + q[3])
def __sub__(self, q):
if hasattr(q, "__float__"):
return Quad(self.ul - q, self.ur - q, self.ll - q, self.lr - q)
if len(p) != 4:
raise ValueError("Quad: bad seq len")
return Quad(self.ul - q[0], self.ur - q[1], self.ll - q[2], self.lr - q[3])
def __truediv__(self, m):
if hasattr(m, "__float__"):
im = 1. / m
else:
im = util_invert_matrix(m)[1]
if not im:
raise ZeroDivisionError("Matrix not invertible")
q = Quad(self)
q = q.transform(im)
return q
__div__ = __truediv__
def __hash__(self):
return hash(tuple(self))
# some special geometry objects
def EMPTY_RECT():
return Rect(FZ_MAX_INF_RECT, FZ_MAX_INF_RECT, FZ_MIN_INF_RECT, FZ_MIN_INF_RECT)
def INFINITE_RECT():
return Rect(FZ_MIN_INF_RECT, FZ_MIN_INF_RECT, FZ_MAX_INF_RECT, FZ_MAX_INF_RECT)
def EMPTY_IRECT():
return IRect(FZ_MAX_INF_RECT, FZ_MAX_INF_RECT, FZ_MIN_INF_RECT, FZ_MIN_INF_RECT)
def INFINITE_IRECT():
return IRect(FZ_MIN_INF_RECT, FZ_MIN_INF_RECT, FZ_MAX_INF_RECT, FZ_MAX_INF_RECT)
def INFINITE_QUAD():
return INFINITE_RECT().quad
def EMPTY_QUAD():
return EMPTY_RECT().quad
#------------------------------------------------------------------------------
# Class describing a PDF form field ("widget")
#------------------------------------------------------------------------------
class Widget(object):
def __init__(self):
self.thisown = True
self.border_color = None
self.border_style = "S"
self.border_width = 0
self.border_dashes = None
self.choice_values = None # choice fields only
self.rb_parent = None # radio buttons only: xref of owning parent
self.field_name = None # field name
self.field_label = None # field label
self.field_value = None
self.field_flags = 0
self.field_display = 0
self.field_type = 0 # valid range 1 through 7
self.field_type_string = None # field type as string
self.fill_color = None
self.button_caption = None # button caption
self.is_signed = None # True / False if signature
self.text_color = (0, 0, 0)
self.text_font = "Helv"
self.text_fontsize = 0
self.text_maxlen = 0 # text fields only
self.text_format = 0 # text fields only
self._text_da = "" # /DA = default apparance
self.script = None # JavaScript (/A)
self.script_stroke = None # JavaScript (/AA/K)
self.script_format = None # JavaScript (/AA/F)
self.script_change = None # JavaScript (/AA/V)
self.script_calc = None # JavaScript (/AA/C)
self.script_blur = None # JavaScript (/AA/Bl)
self.script_focus = None # JavaScript (/AA/Fo)
self.rect = None # annot value
self.xref = 0 # annot value
def _validate(self):
"""Validate the class entries.
"""
if (self.rect.is_infinite
or self.rect.is_empty
):
raise ValueError("bad rect")
if not self.field_name:
raise ValueError("field name missing")
if self.field_label == "Unnamed":
self.field_label = None
CheckColor(self.border_color)
CheckColor(self.fill_color)
if not self.text_color:
self.text_color = (0, 0, 0)
CheckColor(self.text_color)
if not self.border_width:
self.border_width = 0
if not self.text_fontsize:
self.text_fontsize = 0
self.border_style = self.border_style.upper()[0:1]
# standardize content of JavaScript entries
btn_type = self.field_type in (
PDF_WIDGET_TYPE_BUTTON,
PDF_WIDGET_TYPE_CHECKBOX,
PDF_WIDGET_TYPE_RADIOBUTTON
)
if not self.script:
self.script = None
elif type(self.script) is not str:
raise ValueError("script content must be a string")
# buttons cannot have the following script actions
if btn_type or not self.script_calc:
self.script_calc = None
elif type(self.script_calc) is not str:
raise ValueError("script_calc content must be a string")
if btn_type or not self.script_change:
self.script_change = None
elif type(self.script_change) is not str:
raise ValueError("script_change content must be a string")
if btn_type or not self.script_format:
self.script_format = None
elif type(self.script_format) is not str:
raise ValueError("script_format content must be a string")
if btn_type or not self.script_stroke:
self.script_stroke = None
elif type(self.script_stroke) is not str:
raise ValueError("script_stroke content must be a string")
if btn_type or not self.script_blur:
self.script_blur = None
elif type(self.script_blur) is not str:
raise ValueError("script_blur content must be a string")
if btn_type or not self.script_focus:
self.script_focus = None
elif type(self.script_focus) is not str:
raise ValueError("script_focus content must be a string")
self._checker() # any field_type specific checks
def _adjust_font(self):
"""Ensure text_font is correctly spelled if empty or from our list.
Otherwise assume the font is in an existing field.
"""
if not self.text_font:
self.text_font = "Helv"
return
doc = self.parent.parent
for f in doc.FormFonts + ["Cour", "TiRo", "Helv", "ZaDb"]:
if self.text_font.lower() == f.lower():
self.text_font = f
return
self.text_font = "Helv"
return
def _parse_da(self):
"""Extract font name, size and color from default appearance string (/DA object).
Equivalent to 'pdf_parse_default_appearance' function in MuPDF's 'pdf-annot.c'.
"""
if not self._text_da:
return
font = "Helv"
fsize = 0
col = (0, 0, 0)
dat = self._text_da.split() # split on any whitespace
for i, item in enumerate(dat):
if item == "Tf":
font = dat[i - 2][1:]
fsize = float(dat[i - 1])
dat[i] = dat[i-1] = dat[i-2] = ""
continue
if item == "g": # unicolor text
col = [(float(dat[i - 1]))]
dat[i] = dat[i-1] = ""
continue
if item == "rg": # RGB colored text
col = [float(f) for f in dat[i - 3:i]]
dat[i] = dat[i-1] = dat[i-2] = dat[i-3] = ""
continue
self.text_font = font
self.text_fontsize = fsize
self.text_color = col
self._text_da = ""
return
def _checker(self):
"""Any widget type checks.
"""
if self.field_type not in range(1, 8):
raise ValueError("bad field type")
# if setting a radio button to ON, first set Off all buttons
# in the group - this is not done by MuPDF:
if self.field_type == PDF_WIDGET_TYPE_RADIOBUTTON and self.field_value not in (False, "Off") and hasattr(self, "parent"):
# so we are about setting this button to ON/True
# check other buttons in same group and set them to 'Off'
doc = self.parent.parent
kids_type, kids_value = doc.xref_get_key(self.xref, "Parent/Kids")
if kids_type == "array":
xrefs = tuple(map(int, kids_value[1:-1].replace("0 R","").split()))
for xref in xrefs:
if xref != self.xref:
doc.xref_set_key(xref, "AS", "/Off")
# the calling method will now set the intended button to on and
# will find everything prepared for correct functioning.
def update(self):
"""Reflect Python object in the PDF.
"""
doc = self.parent.parent
self._validate()
self._adjust_font() # ensure valid text_font name
# now create the /DA string
self._text_da = ""
if len(self.text_color) == 3:
fmt = "{:g} {:g} {:g} rg /{f:s} {s:g} Tf" + self._text_da
elif len(self.text_color) == 1:
fmt = "{:g} g /{f:s} {s:g} Tf" + self._text_da
elif len(self.text_color) == 4:
fmt = "{:g} {:g} {:g} {:g} k /{f:s} {s:g} Tf" + self._text_da
self._text_da = fmt.format(*self.text_color, f=self.text_font,
s=self.text_fontsize)
# if widget has a '/AA/C' script, make sure it is in the '/CO'
# array of the '/AcroForm' dictionary.
if self.script_calc: # there is a "calculation" script:
# make sure we are in the /CO array
util_ensure_widget_calc(self._annot)
# finally update the widget
TOOLS._save_widget(self._annot, self)
self._text_da = ""
def button_states(self):
"""Return the on/off state names for button widgets.
A button may have 'normal' or 'pressed down' appearances. While the 'Off'
state is usually called like this, the 'On' state is often given a name
relating to the functional context.
"""
if self.field_type not in (2, 5):
return None # no button type
if hasattr(self, "parent"): # field already exists on page
doc = self.parent.parent
else:
return None
xref = self.xref
states = {"normal": None, "down": None}
APN = doc.xref_get_key(xref, "AP/N")
if APN[0] == "dict":
nstates = []
APN = APN[1][2:-2]
apnt = APN.split("/")[1:]
for x in apnt:
nstates.append(x.split()[0])
states["normal"] = nstates
if APN[0] == "xref":
nstates = []
nxref = int(APN[1].split(" ")[0])
APN = doc.xref_object(nxref)
apnt = APN.split("/")[1:]
for x in apnt:
nstates.append(x.split()[0])
states["normal"] = nstates
APD = doc.xref_get_key(xref, "AP/D")
if APD[0] == "dict":
dstates = []
APD = APD[1][2:-2]
apdt = APD.split("/")[1:]
for x in apdt:
dstates.append(x.split()[0])
states["down"] = dstates
if APD[0] == "xref":
dstates = []
dxref = int(APD[1].split(" ")[0])
APD = doc.xref_object(dxref)
apdt = APD.split("/")[1:]
for x in apdt:
dstates.append(x.split()[0])
states["down"] = dstates
return states
def on_state(self):
"""Return the "On" value for button widgets.
This is useful for radio buttons mainly. Checkboxes will always return
"Yes". Radio buttons will return the string that is unequal to "Off"
as returned by method button_states().
If the radio button is new / being created, it does not yet have an
"On" value. In this case, a warning is shown and True is returned.
"""
if self.field_type not in (2, 5):
return None # no checkbox or radio button
if self.field_type == 2:
return "Yes"
bstate = self.button_states()
if bstate==None:
bstate = {}
for k in bstate.keys():
for v in bstate[k]:
if v != "Off":
return v
print("warning: radio button has no 'On' value.")
return True
def reset(self):
"""Reset the field value to its default.
"""
TOOLS._reset_widget(self._annot)
def __repr__(self):
return "'%s' widget on %s" % (self.field_type_string, str(self.parent))
def __del__(self):
if hasattr(self, "_annot"):
del self._annot
@property
def next(self):
return self._annot.next
# ------------------------------------------------------------------------
# Copyright 2020-2022, Harald Lieder, mailto:harald.lieder@outlook.com
# License: GNU AFFERO GPL 3.0, https://www.gnu.org/licenses/agpl-3.0.html
#
# Part of "PyMuPDF", a Python binding for "MuPDF" (http://mupdf.com), a
# lightweight PDF, XPS, and E-book viewer, renderer and toolkit which is
# maintained and developed by Artifex Software, Inc. https://artifex.com.
# ------------------------------------------------------------------------
# ------------------------------------------------------------------------------
# Various PDF Optional Content Flags
# ------------------------------------------------------------------------------
PDF_OC_ON = 0
PDF_OC_TOGGLE = 1
PDF_OC_OFF = 2
# ------------------------------------------------------------------------------
# link kinds and link flags
# ------------------------------------------------------------------------------
LINK_NONE = 0
LINK_GOTO = 1
LINK_URI = 2
LINK_LAUNCH = 3
LINK_NAMED = 4
LINK_GOTOR = 5
LINK_FLAG_L_VALID = 1
LINK_FLAG_T_VALID = 2
LINK_FLAG_R_VALID = 4
LINK_FLAG_B_VALID = 8
LINK_FLAG_FIT_H = 16
LINK_FLAG_FIT_V = 32
LINK_FLAG_R_IS_ZOOM = 64
# ------------------------------------------------------------------------------
# Text handling flags
# ------------------------------------------------------------------------------
TEXT_ALIGN_LEFT = 0
TEXT_ALIGN_CENTER = 1
TEXT_ALIGN_RIGHT = 2
TEXT_ALIGN_JUSTIFY = 3
TEXT_OUTPUT_TEXT = 0
TEXT_OUTPUT_HTML = 1
TEXT_OUTPUT_JSON = 2
TEXT_OUTPUT_XML = 3
TEXT_OUTPUT_XHTML = 4
TEXT_PRESERVE_LIGATURES = 1
TEXT_PRESERVE_WHITESPACE = 2
TEXT_PRESERVE_IMAGES = 4
TEXT_INHIBIT_SPACES = 8
TEXT_DEHYPHENATE = 16
TEXT_PRESERVE_SPANS = 32
TEXT_MEDIABOX_CLIP = 64
TEXT_CID_FOR_UNKNOWN_UNICODE = 128
TEXTFLAGS_WORDS = (0
| TEXT_PRESERVE_LIGATURES
| TEXT_PRESERVE_WHITESPACE
| TEXT_MEDIABOX_CLIP
| TEXT_CID_FOR_UNKNOWN_UNICODE
)
TEXTFLAGS_BLOCKS = (0
| TEXT_PRESERVE_LIGATURES
| TEXT_PRESERVE_WHITESPACE
| TEXT_MEDIABOX_CLIP
| TEXT_CID_FOR_UNKNOWN_UNICODE
)
TEXTFLAGS_DICT = (0
| TEXT_PRESERVE_LIGATURES
| TEXT_PRESERVE_WHITESPACE
| TEXT_MEDIABOX_CLIP
| TEXT_PRESERVE_IMAGES
| TEXT_CID_FOR_UNKNOWN_UNICODE
)
TEXTFLAGS_RAWDICT = TEXTFLAGS_DICT
TEXTFLAGS_SEARCH = (0
| TEXT_PRESERVE_LIGATURES
| TEXT_PRESERVE_WHITESPACE
| TEXT_MEDIABOX_CLIP
| TEXT_DEHYPHENATE
| TEXT_CID_FOR_UNKNOWN_UNICODE
)
TEXTFLAGS_HTML = (0
| TEXT_PRESERVE_LIGATURES
| TEXT_PRESERVE_WHITESPACE
| TEXT_MEDIABOX_CLIP
| TEXT_PRESERVE_IMAGES
| TEXT_CID_FOR_UNKNOWN_UNICODE
)
TEXTFLAGS_XHTML = (0
| TEXT_PRESERVE_LIGATURES
| TEXT_PRESERVE_WHITESPACE
| TEXT_MEDIABOX_CLIP
| TEXT_PRESERVE_IMAGES
| TEXT_CID_FOR_UNKNOWN_UNICODE
)
TEXTFLAGS_XML = (0
| TEXT_PRESERVE_LIGATURES
| TEXT_PRESERVE_WHITESPACE
| TEXT_MEDIABOX_CLIP
| TEXT_CID_FOR_UNKNOWN_UNICODE
)
TEXTFLAGS_TEXT = (0
| TEXT_PRESERVE_LIGATURES
| TEXT_PRESERVE_WHITESPACE
| TEXT_MEDIABOX_CLIP
| TEXT_CID_FOR_UNKNOWN_UNICODE
)
# ------------------------------------------------------------------------------
# Simple text encoding options
# ------------------------------------------------------------------------------
TEXT_ENCODING_LATIN = 0
TEXT_ENCODING_GREEK = 1
TEXT_ENCODING_CYRILLIC = 2
# ------------------------------------------------------------------------------
# Stamp annotation icon numbers
# ------------------------------------------------------------------------------
STAMP_Approved = 0
STAMP_AsIs = 1
STAMP_Confidential = 2
STAMP_Departmental = 3
STAMP_Experimental = 4
STAMP_Expired = 5
STAMP_Final = 6
STAMP_ForComment = 7
STAMP_ForPublicRelease = 8
STAMP_NotApproved = 9
STAMP_NotForPublicRelease = 10
STAMP_Sold = 11
STAMP_TopSecret = 12
STAMP_Draft = 13
# ------------------------------------------------------------------------------
# Base 14 font names and dictionary
# ------------------------------------------------------------------------------
Base14_fontnames = (
"Courier",
"Courier-Oblique",
"Courier-Bold",
"Courier-BoldOblique",
"Helvetica",
"Helvetica-Oblique",
"Helvetica-Bold",
"Helvetica-BoldOblique",
"Times-Roman",
"Times-Italic",
"Times-Bold",
"Times-BoldItalic",
"Symbol",
"ZapfDingbats",
)
Base14_fontdict = {}
for f in Base14_fontnames:
Base14_fontdict[f.lower()] = f
del f
Base14_fontdict["helv"] = "Helvetica"
Base14_fontdict["heit"] = "Helvetica-Oblique"
Base14_fontdict["hebo"] = "Helvetica-Bold"
Base14_fontdict["hebi"] = "Helvetica-BoldOblique"
Base14_fontdict["cour"] = "Courier"
Base14_fontdict["coit"] = "Courier-Oblique"
Base14_fontdict["cobo"] = "Courier-Bold"
Base14_fontdict["cobi"] = "Courier-BoldOblique"
Base14_fontdict["tiro"] = "Times-Roman"
Base14_fontdict["tibo"] = "Times-Bold"
Base14_fontdict["tiit"] = "Times-Italic"
Base14_fontdict["tibi"] = "Times-BoldItalic"
Base14_fontdict["symb"] = "Symbol"
Base14_fontdict["zadb"] = "ZapfDingbats"
annot_skel = {
"goto1": "<</A<</S/GoTo/D[%i 0 R/XYZ %g %g %g]>>/Rect[%s]/BS<</W 0>>/Subtype/Link>>",
"goto2": "<</A<</S/GoTo/D%s>>/Rect[%s]/BS<</W 0>>/Subtype/Link>>",
"gotor1": "<</A<</S/GoToR/D[%i /XYZ %g %g %g]/F<</F(%s)/UF(%s)/Type/Filespec>>>>/Rect[%s]/BS<</W 0>>/Subtype/Link>>",
"gotor2": "<</A<</S/GoToR/D%s/F(%s)>>/Rect[%s]/BS<</W 0>>/Subtype/Link>>",
"launch": "<</A<</S/Launch/F<</F(%s)/UF(%s)/Type/Filespec>>>>/Rect[%s]/BS<</W 0>>/Subtype/Link>>",
"uri": "<</A<</S/URI/URI(%s)>>/Rect[%s]/BS<</W 0>>/Subtype/Link>>",
"named": "<</A<</S/Named/N/%s/Type/Action>>/Rect[%s]/BS<</W 0>>/Subtype/Link>>",
}
class FileDataError(RuntimeError):
"""Raised for documents with file structure issues."""
pass
class FileNotFoundError(RuntimeError):
"""Raised if file does not exist."""
pass
class EmptyFileError(FileDataError):
"""Raised when creating documents from zero-length data."""
pass
# propagate exception class to C-level code
_set_FileDataError(FileDataError)
def css_for_pymupdf_font(
fontcode: str, *, CSS: OptStr = None, archive: AnyType = None, name: OptStr = None
) -> str:
"""Create @font-face items for the given fontcode of pymupdf-fonts.
Adds @font-face support for fonts contained in package pymupdf-fonts.
Creates a CSS font-family for all fonts starting with string 'fontcode'.
Note:
The font naming convention in package pymupdf-fonts is "fontcode<sf>",
where the suffix "sf" is either empty or one of "it", "bo" or "bi".
These suffixes thus represent the regular, italic, bold or bold-italic
variants of a font. For example, font code "notos" refers to fonts
"notos" - "Noto Sans Regular"
"notosit" - "Noto Sans Italic"
"notosbo" - "Noto Sans Bold"
"notosbi" - "Noto Sans Bold Italic"
This function creates four CSS @font-face definitions and collectively
assigns the font-family name "notos" to them (or the "name" value).
All fitting font buffers of the pymupdf-fonts package are placed / added
to the archive provided as parameter.
To use the font in fitz.Story, execute 'set_font(fontcode)'. The correct
font weight (bold) or style (italic) will automatically be selected.
Expects and returns the CSS source, with the new CSS definitions appended.
Args:
fontcode: (str) font code for naming the font variants to include.
E.g. "fig" adds notos, notosi, notosb, notosbi fonts.
A maximum of 4 font variants is accepted.
CSS: (str) CSS string to add @font-face definitions to.
archive: (Archive, mandatory) where to place the font buffers.
name: (str) use this as family-name instead of 'fontcode'.
Returns:
Modified CSS, with appended @font-face statements for each font variant
of fontcode.
Fontbuffers associated with "fontcode" will be added to 'archive'.
"""
# @font-face template string
CSSFONT = "\n@font-face {font-family: %s; src: url(%s);%s%s}\n"
if not type(archive) is Archive:
raise ValueError("'archive' must be an Archive")
if CSS == None:
CSS = ""
# select font codes starting with the pass-in string
font_keys = [k for k in fitz_fontdescriptors.keys() if k.startswith(fontcode)]
if font_keys == []:
raise ValueError(f"No font code '{fontcode}' found in pymupdf-fonts.")
if len(font_keys) > 4:
raise ValueError("fontcode too short")
if name == None: # use this name for font-family
name = fontcode
for fkey in font_keys:
font = fitz_fontdescriptors[fkey]
bold = font["bold"] # determine font property
italic = font["italic"] # determine font property
fbuff = font["loader"]() # load the fontbuffer
archive.add(fbuff, fkey) # update the archive
bold_text = "font-weight: bold;" if bold else ""
italic_text = "font-style: italic;" if italic else ""
CSS += CSSFONT % (name, fkey, bold_text, italic_text)
return CSS
def get_text_length(text: str, fontname: str ="helv", fontsize: float =11, encoding: int =0) -> float:
"""Calculate length of a string for a built-in font.
Args:
fontname: name of the font.
fontsize: font size points.
encoding: encoding to use, 0=Latin (default), 1=Greek, 2=Cyrillic.
Returns:
(float) length of text.
"""
fontname = fontname.lower()
basename = Base14_fontdict.get(fontname, None)
glyphs = None
if basename == "Symbol":
glyphs = symbol_glyphs
if basename == "ZapfDingbats":
glyphs = zapf_glyphs
if glyphs is not None:
w = sum([glyphs[ord(c)][1] if ord(c) < 256 else glyphs[183][1] for c in text])
return w * fontsize
if fontname in Base14_fontdict.keys():
return util_measure_string(
text, Base14_fontdict[fontname], fontsize, encoding
)
if fontname in (
"china-t",
"china-s",
"china-ts",
"china-ss",
"japan",
"japan-s",
"korea",
"korea-s",
):
return len(text) * fontsize
raise ValueError("Font '%s' is unsupported" % fontname)
# ------------------------------------------------------------------------------
# Glyph list for the built-in font 'ZapfDingbats'
# ------------------------------------------------------------------------------
zapf_glyphs = (
(183, 0.788),
(183, 0.788),
(183, 0.788),
(183, 0.788),
(183, 0.788),
(183, 0.788),
(183, 0.788),
(183, 0.788),
(183, 0.788),
(183, 0.788),
(183, 0.788),
(183, 0.788),
(183, 0.788),
(183, 0.788),
(183, 0.788),
(183, 0.788),
(183, 0.788),
(183, 0.788),
(183, 0.788),
(183, 0.788),
(183, 0.788),
(183, 0.788),
(183, 0.788),
(183, 0.788),
(183, 0.788),
(183, 0.788),
(183, 0.788),
(183, 0.788),
(183, 0.788),
(183, 0.788),
(183, 0.788),
(183, 0.788),
(32, 0.278),
(33, 0.974),
(34, 0.961),
(35, 0.974),
(36, 0.98),
(37, 0.719),
(38, 0.789),
(39, 0.79),
(40, 0.791),
(41, 0.69),
(42, 0.96),
(43, 0.939),
(44, 0.549),
(45, 0.855),
(46, 0.911),
(47, 0.933),
(48, 0.911),
(49, 0.945),
(50, 0.974),
(51, 0.755),
(52, 0.846),
(53, 0.762),
(54, 0.761),
(55, 0.571),
(56, 0.677),
(57, 0.763),
(58, 0.76),
(59, 0.759),
(60, 0.754),
(61, 0.494),
(62, 0.552),
(63, 0.537),
(64, 0.577),
(65, 0.692),
(66, 0.786),
(67, 0.788),
(68, 0.788),
(69, 0.79),
(70, 0.793),
(71, 0.794),
(72, 0.816),
(73, 0.823),
(74, 0.789),
(75, 0.841),
(76, 0.823),
(77, 0.833),
(78, 0.816),
(79, 0.831),
(80, 0.923),
(81, 0.744),
(82, 0.723),
(83, 0.749),
(84, 0.79),
(85, 0.792),
(86, 0.695),
(87, 0.776),
(88, 0.768),
(89, 0.792),
(90, 0.759),
(91, 0.707),
(92, 0.708),
(93, 0.682),
(94, 0.701),
(95, 0.826),
(96, 0.815),
(97, 0.789),
(98, 0.789),
(99, 0.707),
(100, 0.687),
(101, 0.696),
(102, 0.689),
(103, 0.786),
(104, 0.787),
(105, 0.713),
(106, 0.791),
(107, 0.785),
(108, 0.791),
(109, 0.873),
(110, 0.761),
(111, 0.762),
(112, 0.762),
(113, 0.759),
(114, 0.759),
(115, 0.892),
(116, 0.892),
(117, 0.788),
(118, 0.784),
(119, 0.438),
(120, 0.138),
(121, 0.277),
(122, 0.415),
(123, 0.392),
(124, 0.392),
(125, 0.668),
(126, 0.668),
(183, 0.788),
(183, 0.788),
(183, 0.788),
(183, 0.788),
(183, 0.788),
(183, 0.788),
(183, 0.788),
(183, 0.788),
(183, 0.788),
(183, 0.788),
(183, 0.788),
(183, 0.788),
(183, 0.788),
(183, 0.788),
(183, 0.788),
(183, 0.788),
(183, 0.788),
(183, 0.788),
(183, 0.788),
(183, 0.788),
(183, 0.788),
(183, 0.788),
(183, 0.788),
(183, 0.788),
(183, 0.788),
(183, 0.788),
(183, 0.788),
(183, 0.788),
(183, 0.788),
(183, 0.788),
(183, 0.788),
(183, 0.788),
(183, 0.788),
(183, 0.788),
(161, 0.732),
(162, 0.544),
(163, 0.544),
(164, 0.91),
(165, 0.667),
(166, 0.76),
(167, 0.76),
(168, 0.776),
(169, 0.595),
(170, 0.694),
(171, 0.626),
(172, 0.788),
(173, 0.788),
(174, 0.788),
(175, 0.788),
(176, 0.788),
(177, 0.788),
(178, 0.788),
(179, 0.788),
(180, 0.788),
(181, 0.788),
(182, 0.788),
(183, 0.788),
(184, 0.788),
(185, 0.788),
(186, 0.788),
(187, 0.788),
(188, 0.788),
(189, 0.788),
(190, 0.788),
(191, 0.788),
(192, 0.788),
(193, 0.788),
(194, 0.788),
(195, 0.788),
(196, 0.788),
(197, 0.788),
(198, 0.788),
(199, 0.788),
(200, 0.788),
(201, 0.788),
(202, 0.788),
(203, 0.788),
(204, 0.788),
(205, 0.788),
(206, 0.788),
(207, 0.788),
(208, 0.788),
(209, 0.788),
(210, 0.788),
(211, 0.788),
(212, 0.894),
(213, 0.838),
(214, 1.016),
(215, 0.458),
(216, 0.748),
(217, 0.924),
(218, 0.748),
(219, 0.918),
(220, 0.927),
(221, 0.928),
(222, 0.928),
(223, 0.834),
(224, 0.873),
(225, 0.828),
(226, 0.924),
(227, 0.924),
(228, 0.917),
(229, 0.93),
(230, 0.931),
(231, 0.463),
(232, 0.883),
(233, 0.836),
(234, 0.836),
(235, 0.867),
(236, 0.867),
(237, 0.696),
(238, 0.696),
(239, 0.874),
(183, 0.788),
(241, 0.874),
(242, 0.76),
(243, 0.946),
(244, 0.771),
(245, 0.865),
(246, 0.771),
(247, 0.888),
(248, 0.967),
(249, 0.888),
(250, 0.831),
(251, 0.873),
(252, 0.927),
(253, 0.97),
(183, 0.788),
(183, 0.788),
)
# ------------------------------------------------------------------------------
# Glyph list for the built-in font 'Symbol'
# ------------------------------------------------------------------------------
symbol_glyphs = (
(183, 0.46),
(183, 0.46),
(183, 0.46),
(183, 0.46),
(183, 0.46),
(183, 0.46),
(183, 0.46),
(183, 0.46),
(183, 0.46),
(183, 0.46),
(183, 0.46),
(183, 0.46),
(183, 0.46),
(183, 0.46),
(183, 0.46),
(183, 0.46),
(183, 0.46),
(183, 0.46),
(183, 0.46),
(183, 0.46),
(183, 0.46),
(183, 0.46),
(183, 0.46),
(183, 0.46),
(183, 0.46),
(183, 0.46),
(183, 0.46),
(183, 0.46),
(183, 0.46),
(183, 0.46),
(183, 0.46),
(183, 0.46),
(32, 0.25),
(33, 0.333),
(34, 0.713),
(35, 0.5),
(36, 0.549),
(37, 0.833),
(38, 0.778),
(39, 0.439),
(40, 0.333),
(41, 0.333),
(42, 0.5),
(43, 0.549),
(44, 0.25),
(45, 0.549),
(46, 0.25),
(47, 0.278),
(48, 0.5),
(49, 0.5),
(50, 0.5),
(51, 0.5),
(52, 0.5),
(53, 0.5),
(54, 0.5),
(55, 0.5),
(56, 0.5),
(57, 0.5),
(58, 0.278),
(59, 0.278),
(60, 0.549),
(61, 0.549),
(62, 0.549),
(63, 0.444),
(64, 0.549),
(65, 0.722),
(66, 0.667),
(67, 0.722),
(68, 0.612),
(69, 0.611),
(70, 0.763),
(71, 0.603),
(72, 0.722),
(73, 0.333),
(74, 0.631),
(75, 0.722),
(76, 0.686),
(77, 0.889),
(78, 0.722),
(79, 0.722),
(80, 0.768),
(81, 0.741),
(82, 0.556),
(83, 0.592),
(84, 0.611),
(85, 0.69),
(86, 0.439),
(87, 0.768),
(88, 0.645),
(89, 0.795),
(90, 0.611),
(91, 0.333),
(92, 0.863),
(93, 0.333),
(94, 0.658),
(95, 0.5),
(96, 0.5),
(97, 0.631),
(98, 0.549),
(99, 0.549),
(100, 0.494),
(101, 0.439),
(102, 0.521),
(103, 0.411),
(104, 0.603),
(105, 0.329),
(106, 0.603),
(107, 0.549),
(108, 0.549),
(109, 0.576),
(110, 0.521),
(111, 0.549),
(112, 0.549),
(113, 0.521),
(114, 0.549),
(115, 0.603),
(116, 0.439),
(117, 0.576),
(118, 0.713),
(119, 0.686),
(120, 0.493),
(121, 0.686),
(122, 0.494),
(123, 0.48),
(124, 0.2),
(125, 0.48),
(126, 0.549),
(183, 0.46),
(183, 0.46),
(183, 0.46),
(183, 0.46),
(183, 0.46),
(183, 0.46),
(183, 0.46),
(183, 0.46),
(183, 0.46),
(183, 0.46),
(183, 0.46),
(183, 0.46),
(183, 0.46),
(183, 0.46),
(183, 0.46),
(183, 0.46),
(183, 0.46),
(183, 0.46),
(183, 0.46),
(183, 0.46),
(183, 0.46),
(183, 0.46),
(183, 0.46),
(183, 0.46),
(183, 0.46),
(183, 0.46),
(183, 0.46),
(183, 0.46),
(183, 0.46),
(183, 0.46),
(183, 0.46),
(183, 0.46),
(183, 0.46),
(160, 0.25),
(161, 0.62),
(162, 0.247),
(163, 0.549),
(164, 0.167),
(165, 0.713),
(166, 0.5),
(167, 0.753),
(168, 0.753),
(169, 0.753),
(170, 0.753),
(171, 1.042),
(172, 0.713),
(173, 0.603),
(174, 0.987),
(175, 0.603),
(176, 0.4),
(177, 0.549),
(178, 0.411),
(179, 0.549),
(180, 0.549),
(181, 0.576),
(182, 0.494),
(183, 0.46),
(184, 0.549),
(185, 0.549),
(186, 0.549),
(187, 0.549),
(188, 1),
(189, 0.603),
(190, 1),
(191, 0.658),
(192, 0.823),
(193, 0.686),
(194, 0.795),
(195, 0.987),
(196, 0.768),
(197, 0.768),
(198, 0.823),
(199, 0.768),
(200, 0.768),
(201, 0.713),
(202, 0.713),
(203, 0.713),
(204, 0.713),
(205, 0.713),
(206, 0.713),
(207, 0.713),
(208, 0.768),
(209, 0.713),
(210, 0.79),
(211, 0.79),
(212, 0.89),
(213, 0.823),
(214, 0.549),
(215, 0.549),
(216, 0.713),
(217, 0.603),
(218, 0.603),
(219, 1.042),
(220, 0.987),
(221, 0.603),
(222, 0.987),
(223, 0.603),
(224, 0.494),
(225, 0.329),
(226, 0.79),
(227, 0.79),
(228, 0.786),
(229, 0.713),
(230, 0.384),
(231, 0.384),
(232, 0.384),
(233, 0.384),
(234, 0.384),
(235, 0.384),
(236, 0.494),
(237, 0.494),
(238, 0.494),
(239, 0.494),
(183, 0.46),
(241, 0.329),
(242, 0.274),
(243, 0.686),
(244, 0.686),
(245, 0.686),
(246, 0.384),
(247, 0.549),
(248, 0.384),
(249, 0.384),
(250, 0.384),
(251, 0.384),
(252, 0.494),
(253, 0.494),
(254, 0.494),
(183, 0.46),
)
class linkDest(object):
"""link or outline destination details"""
def __init__(self, obj, rlink):
isExt = obj.is_external
isInt = not isExt
self.dest = ""
self.fileSpec = ""
self.flags = 0
self.isMap = False
self.isUri = False
self.kind = LINK_NONE
self.lt = Point(0, 0)
self.named = ""
self.newWindow = ""
self.page = obj.page
self.rb = Point(0, 0)
self.uri = obj.uri
if rlink and not self.uri.startswith("#"):
self.uri = "#page=%i&zoom=0,%g,%g" % (rlink[0] + 1, rlink[1], rlink[2])
if obj.is_external:
self.page = -1
self.kind = LINK_URI
if not self.uri:
self.page = -1
self.kind = LINK_NONE
if isInt and self.uri:
self.uri = self.uri.replace("&zoom=nan", "&zoom=0")
if self.uri.startswith("#"):
self.named = ""
self.kind = LINK_GOTO
m = re.match('^#page=([0-9]+)&zoom=([0-9.]+),(-?[0-9.]+),(-?[0-9.]+)$', self.uri)
if m:
self.page = int(m.group(1)) - 1
self.lt = Point(float((m.group(3))), float(m.group(4)))
self.flags = self.flags | LINK_FLAG_L_VALID | LINK_FLAG_T_VALID
else:
m = re.match('^#page=([0-9]+)$', self.uri)
if m:
self.page = int(m.group(1)) - 1
else:
self.kind = LINK_NAMED
self.named = self.uri[1:]
else:
self.kind = LINK_NAMED
self.named = self.uri
if obj.is_external:
if self.uri.startswith(("http://", "https://", "mailto:", "ftp://")):
self.isUri = True
self.kind = LINK_URI
elif self.uri.startswith("file://"):
self.fileSpec = self.uri[7:]
self.isUri = False
self.uri = ""
self.kind = LINK_LAUNCH
ftab = self.fileSpec.split("#")
if len(ftab) == 2:
if ftab[1].startswith("page="):
self.kind = LINK_GOTOR
self.fileSpec = ftab[0]
self.page = int(ftab[1][5:]) - 1
else:
self.isUri = True
self.kind = LINK_LAUNCH
# -------------------------------------------------------------------------------
# "Now" timestamp in PDF Format
# -------------------------------------------------------------------------------
def get_pdf_now() -> str:
import time
tz = "%s'%s'" % (
str(abs(time.altzone // 3600)).rjust(2, "0"),
str((abs(time.altzone // 60) % 60)).rjust(2, "0"),
)
tstamp = time.strftime("D:%Y%m%d%H%M%S", time.localtime())
if time.altzone > 0:
tstamp += "-" + tz
elif time.altzone < 0:
tstamp += "+" + tz
else:
pass
return tstamp
def get_pdf_str(s: str) -> str:
""" Return a PDF string depending on its coding.
Notes:
Returns a string bracketed with either "()" or "<>" for hex values.
If only ascii then "(original)" is returned, else if only 8 bit chars
then "(original)" with interspersed octal strings \nnn is returned,
else a string "<FEFF[hexstring]>" is returned, where [hexstring] is the
UTF-16BE encoding of the original.
"""
if not bool(s):
return "()"
def make_utf16be(s):
r = bytearray([254, 255]) + bytearray(s, "UTF-16BE")
return "<" + r.hex() + ">" # brackets indicate hex
# The following either returns the original string with mixed-in
# octal numbers \nnn for chars outside the ASCII range, or returns
# the UTF-16BE BOM version of the string.
r = ""
for c in s:
oc = ord(c)
if oc > 255: # shortcut if beyond 8-bit code range
return make_utf16be(s)
if oc > 31 and oc < 127: # in ASCII range
if c in ("(", ")", "\\"): # these need to be escaped
r += "\\"
r += c
continue
if oc > 127: # beyond ASCII
r += "\\%03o" % oc
continue
# now the white spaces
if oc == 8: # backspace
r += "\\b"
elif oc == 9: # tab
r += "\\t"
elif oc == 10: # line feed
r += "\\n"
elif oc == 12: # form feed
r += "\\f"
elif oc == 13: # carriage return
r += "\\r"
else:
r += "\\267" # unsupported: replace by 0xB7
return "(" + r + ")"
def getTJstr(text: str, glyphs: typing.Union[list, tuple, None], simple: bool, ordering: int) -> str:
""" Return a PDF string enclosed in [] brackets, suitable for the PDF TJ
operator.
Notes:
The input string is converted to either 2 or 4 hex digits per character.
Args:
simple: no glyphs: 2-chars, use char codes as the glyph
glyphs: 2-chars, use glyphs instead of char codes (Symbol,
ZapfDingbats)
not simple: ordering < 0: 4-chars, use glyphs not char codes
ordering >=0: a CJK font! 4 chars, use char codes as glyphs
"""
if text.startswith("[<") and text.endswith(">]"): # already done
return text
if not bool(text):
return "[<>]"
if simple: # each char or its glyph is coded as a 2-byte hex
if glyphs is None: # not Symbol, not ZapfDingbats: use char code
otxt = "".join(["%02x" % ord(c) if ord(c) < 256 else "b7" for c in text])
else: # Symbol or ZapfDingbats: use glyphs
otxt = "".join(
["%02x" % glyphs[ord(c)][0] if ord(c) < 256 else "b7" for c in text]
)
return "[<" + otxt + ">]"
# non-simple fonts: each char or its glyph is coded as 4-byte hex
if ordering < 0: # not a CJK font: use the glyphs
otxt = "".join(["%04x" % glyphs[ord(c)][0] for c in text])
else: # CJK: use the char codes
otxt = "".join(["%04x" % ord(c) for c in text])
return "[<" + otxt + ">]"
def paper_sizes():
"""Known paper formats @ 72 dpi as a dictionary. Key is the format string
like "a4" for ISO-A4. Value is the tuple (width, height).
Information taken from the following web sites:
www.din-formate.de
www.din-formate.info/amerikanische-formate.html
www.directtools.de/wissen/normen/iso.htm
"""
return {
"a0": (2384, 3370),
"a1": (1684, 2384),
"a10": (74, 105),
"a2": (1191, 1684),
"a3": (842, 1191),
"a4": (595, 842),
"a5": (420, 595),
"a6": (298, 420),
"a7": (210, 298),
"a8": (147, 210),
"a9": (105, 147),
"b0": (2835, 4008),
"b1": (2004, 2835),
"b10": (88, 125),
"b2": (1417, 2004),
"b3": (1001, 1417),
"b4": (709, 1001),
"b5": (499, 709),
"b6": (354, 499),
"b7": (249, 354),
"b8": (176, 249),
"b9": (125, 176),
"c0": (2599, 3677),
"c1": (1837, 2599),
"c10": (79, 113),
"c2": (1298, 1837),
"c3": (918, 1298),
"c4": (649, 918),
"c5": (459, 649),
"c6": (323, 459),
"c7": (230, 323),
"c8": (162, 230),
"c9": (113, 162),
"card-4x6": (288, 432),
"card-5x7": (360, 504),
"commercial": (297, 684),
"executive": (522, 756),
"invoice": (396, 612),
"ledger": (792, 1224),
"legal": (612, 1008),
"legal-13": (612, 936),
"letter": (612, 792),
"monarch": (279, 540),
"tabloid-extra": (864, 1296),
}
def paper_size(s: str) -> tuple:
"""Return a tuple (width, height) for a given paper format string.
Notes:
'A4-L' will return (842, 595), the values for A4 landscape.
Suffix '-P' and no suffix return the portrait tuple.
"""
size = s.lower()
f = "p"
if size.endswith("-l"):
f = "l"
size = size[:-2]
if size.endswith("-p"):
size = size[:-2]
rc = paper_sizes().get(size, (-1, -1))
if f == "p":
return rc
return (rc[1], rc[0])
def paper_rect(s: str) -> Rect:
"""Return a Rect for the paper size indicated in string 's'. Must conform to the argument of method 'PaperSize', which will be invoked.
"""
width, height = paper_size(s)
return Rect(0.0, 0.0, width, height)
def CheckParent(o: typing.Any):
if getattr(o, "parent", None) == None:
raise ValueError("orphaned object: parent is None")
def EnsureOwnership(o: typing.Any):
if not getattr(o, "thisown", False):
raise RuntimeError("object destroyed")
def CheckColor(c: OptSeq):
if c:
if (
type(c) not in (list, tuple)
or len(c) not in (1, 3, 4)
or min(c) < 0
or max(c) > 1
):
raise ValueError("need 1, 3 or 4 color components in range 0 to 1")
def ColorCode(c: typing.Union[list, tuple, float, None], f: str) -> str:
if not c:
return ""
if hasattr(c, "__float__"):
c = (c,)
CheckColor(c)
if len(c) == 1:
s = "%g " % c[0]
return s + "G " if f == "c" else s + "g "
if len(c) == 3:
s = "%g %g %g " % tuple(c)
return s + "RG " if f == "c" else s + "rg "
s = "%g %g %g %g " % tuple(c)
return s + "K " if f == "c" else s + "k "
def JM_TUPLE(o: typing.Sequence) -> tuple:
return tuple(map(lambda x: round(x, 5) if abs(x) >= 1e-4 else 0, o))
def JM_TUPLE3(o: typing.Sequence) -> tuple:
return tuple(map(lambda x: round(x, 3) if abs(x) >= 1e-3 else 0, o))
def CheckRect(r: typing.Any) -> bool:
"""Check whether an object is non-degenerate rect-like.
It must be a sequence of 4 numbers.
"""
try:
r = Rect(r)
except:
return False
return not (r.is_empty or r.is_infinite)
def CheckQuad(q: typing.Any) -> bool:
"""Check whether an object is convex, not empty quad-like.
It must be a sequence of 4 number pairs.
"""
try:
q0 = Quad(q)
except:
return False
return q0.is_convex
def CheckMarkerArg(quads: typing.Any) -> tuple:
if CheckRect(quads):
r = Rect(quads)
return (r.quad,)
if CheckQuad(quads):
return (quads,)
for q in quads:
if not (CheckRect(q) or CheckQuad(q)):
raise ValueError("bad quads entry")
return quads
def CheckMorph(o: typing.Any) -> bool:
if not bool(o):
return False
if not (type(o) in (list, tuple) and len(o) == 2):
raise ValueError("morph must be a sequence of length 2")
if not (len(o[0]) == 2 and len(o[1]) == 6):
raise ValueError("invalid morph parm 0")
if not o[1][4] == o[1][5] == 0:
raise ValueError("invalid morph parm 1")
return True
def CheckFont(page: "struct Page *", fontname: str) -> tuple:
"""Return an entry in the page's font list if reference name matches.
"""
for f in page.get_fonts():
if f[4] == fontname:
return f
def CheckFontInfo(doc: "struct Document *", xref: int) -> list:
"""Return a font info if present in the document.
"""
for f in doc.FontInfos:
if xref == f[0]:
return f
def UpdateFontInfo(doc: "struct Document *", info: typing.Sequence):
xref = info[0]
found = False
for i, fi in enumerate(doc.FontInfos):
if fi[0] == xref:
found = True
break
if found:
doc.FontInfos[i] = info
else:
doc.FontInfos.append(info)
def DUMMY(*args, **kw):
return
def planish_line(p1: point_like, p2: point_like) -> Matrix:
"""Compute matrix which maps line from p1 to p2 to the x-axis, such that it
maintains its length and p1 * matrix = Point(0, 0).
Args:
p1, p2: point_like
Returns:
Matrix which maps p1 to Point(0, 0) and p2 to a point on the x axis at
the same distance to Point(0,0). Will always combine a rotation and a
transformation.
"""
p1 = Point(p1)
p2 = Point(p2)
return Matrix(util_hor_matrix(p1, p2))
def image_profile(img: typing.ByteString) -> dict:
""" Return basic properties of an image.
Args:
img: bytes, bytearray, io.BytesIO object or an opened image file.
Returns:
A dictionary with keys width, height, colorspace.n, bpc, type, ext and size,
where 'type' is the MuPDF image type (0 to 14) and 'ext' the suitable
file extension.
"""
if type(img) is io.BytesIO:
stream = img.getvalue()
elif hasattr(img, "read"):
stream = img.read()
elif type(img) in (bytes, bytearray):
stream = img
else:
raise ValueError("bad argument 'img'")
return TOOLS.image_profile(stream)
def ConversionHeader(i: str, filename: OptStr ="unknown"):
t = i.lower()
html = """<!DOCTYPE html>
<html>
<head>
<style>
body{background-color:gray}
div{position:relative;background-color:white;margin:1em auto}
p{position:absolute;margin:0}
img{position:absolute}
</style>
</head>
<body>\n"""
xml = (
"""<?xml version="1.0"?>
<document name="%s">\n"""
% filename
)
xhtml = """<?xml version="1.0"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<style>
body{background-color:gray}
div{background-color:white;margin:1em;padding:1em}
p{white-space:pre-wrap}
</style>
</head>
<body>\n"""
text = ""
json = '{"document": "%s", "pages": [\n' % filename
if t == "html":
r = html
elif t == "json":
r = json
elif t == "xml":
r = xml
elif t == "xhtml":
r = xhtml
else:
r = text
return r
def ConversionTrailer(i: str):
t = i.lower()
text = ""
json = "]\n}"
html = "</body>\n</html>\n"
xml = "</document>\n"
xhtml = html
if t == "html":
r = html
elif t == "json":
r = json
elif t == "xml":
r = xml
elif t == "xhtml":
r = xhtml
else:
r = text
return r
class ElementPosition(object):
"""Convert a dictionary with element position information to an object."""
def __init__(self):
pass
def __str__(self):
ret = ""
for n, v in self.__dict__.items():
ret += f" {n}={v!r}"
return ret
def make_story_elpos():
return ElementPosition()
def get_highlight_selection(page, start: point_like =None, stop: point_like =None, clip: rect_like =None) -> list:
"""Return rectangles of text lines between two points.
Notes:
The default of 'start' is top-left of 'clip'. The default of 'stop'
is bottom-reight of 'clip'.
Args:
start: start point_like
stop: end point_like, must be 'below' start
clip: consider this rect_like only, default is page rectangle
Returns:
List of line bbox intersections with the area established by the
parameters.
"""
# validate and normalize arguments
if clip is None:
clip = page.rect
clip = Rect(clip)
if start is None:
start = clip.tl
if stop is None:
stop = clip.br
clip.y0 = start.y
clip.y1 = stop.y
if clip.is_empty or clip.is_infinite:
return []
# extract text of page, clip only, no images, expand ligatures
blocks = page.get_text(
"dict", flags=0, clip=clip,
)["blocks"]
lines = [] # will return this list of rectangles
for b in blocks:
bbox = Rect(b["bbox"])
if bbox.is_infinite or bbox.is_empty:
continue
for line in b["lines"]:
bbox = Rect(line["bbox"])
if bbox.is_infinite or bbox.is_empty:
continue
lines.append(bbox)
if lines == []: # did not select anything
return lines
lines.sort(key=lambda bbox: bbox.y1) # sort by vertical positions
# cut off prefix from first line if start point is close to its top
bboxf = lines.pop(0)
if bboxf.y0 - start.y <= 0.1 * bboxf.height: # close enough?
r = Rect(start.x, bboxf.y0, bboxf.br) # intersection rectangle
if not (r.is_empty or r.is_infinite):
lines.insert(0, r) # insert again if not empty
else:
lines.insert(0, bboxf) # insert again
if lines == []: # the list might have been emptied
return lines
# cut off suffix from last line if stop point is close to its bottom
bboxl = lines.pop()
if stop.y - bboxl.y1 <= 0.1 * bboxl.height: # close enough?
r = Rect(bboxl.tl, stop.x, bboxl.y1) # intersection rectangle
if not (r.is_empty or r.is_infinite):
lines.append(r) # append if not empty
else:
lines.append(bboxl) # append again
return lines
def annot_preprocess(page: "Page") -> int:
"""Prepare for annotation insertion on the page.
Returns:
Old page rotation value. Temporarily sets rotation to 0 when required.
"""
CheckParent(page)
if not page.parent.is_pdf:
raise ValueError("is no PDF")
old_rotation = page.rotation
if old_rotation != 0:
page.set_rotation(0)
return old_rotation
def annot_postprocess(page: "Page", annot: "Annot") -> None:
"""Clean up after annotation inertion.
Set ownership flag and store annotation in page annotation dictionary.
"""
annot.parent = weakref.proxy(page)
page._annot_refs[id(annot)] = annot
annot.thisown = True
def sRGB_to_rgb(srgb: int) -> tuple:
"""Convert sRGB color code to an RGB color triple.
There is **no error checking** for performance reasons!
Args:
srgb: (int) RRGGBB (red, green, blue), each color in range(255).
Returns:
Tuple (red, green, blue) each item in intervall 0 <= item <= 255.
"""
r = srgb >> 16
g = (srgb - (r << 16)) >> 8
b = srgb - (r << 16) - (g << 8)
return (r, g, b)
def sRGB_to_pdf(srgb: int) -> tuple:
"""Convert sRGB color code to a PDF color triple.
There is **no error checking** for performance reasons!
Args:
srgb: (int) RRGGBB (red, green, blue), each color in range(255).
Returns:
Tuple (red, green, blue) each item in intervall 0 <= item <= 1.
"""
t = sRGB_to_rgb(srgb)
return t[0] / 255.0, t[1] / 255.0, t[2] / 255.0
def make_table(rect: rect_like =(0, 0, 1, 1), cols: int =1, rows: int =1) -> list:
"""Return a list of (rows x cols) equal sized rectangles.
Notes:
A utility to fill a given area with table cells of equal size.
Args:
rect: rect_like to use as the table area
rows: number of rows
cols: number of columns
Returns:
A list with <rows> items, where each item is a list of <cols>
PyMuPDF Rect objects of equal sizes.
"""
rect = Rect(rect) # ensure this is a Rect
if rect.is_empty or rect.is_infinite:
raise ValueError("rect must be finite and not empty")
tl = rect.tl
height = rect.height / rows # height of one table cell
width = rect.width / cols # width of one table cell
delta_h = (width, 0, width, 0) # diff to next right rect
delta_v = (0, height, 0, height) # diff to next lower rect
r = Rect(tl, tl.x + width, tl.y + height) # first rectangle
# make the first row
row = [r]
for i in range(1, cols):
r += delta_h # build next rect to the right
row.append(r)
# make result, starts with first row
rects = [row]
for i in range(1, rows):
row = rects[i - 1] # take previously appended row
nrow = [] # the new row to append
for r in row: # for each previous cell add its downward copy
nrow.append(r + delta_v)
rects.append(nrow) # append new row to result
return rects
def repair_mono_font(page: "Page", font: "Font") -> None:
"""Repair character spacing for mono fonts.
Notes:
Some mono-spaced fonts are displayed with a too large character
width, e.g. "a b c" instead of "abc". This utility adds an entry
"/DW w" to the descendent font of font. The int w is
taken to be the first width > 0 of the font's unicodes.
This should enforce viewers to use 'w' as the character width.
Args:
page: fitz.Page object.
font: fitz.Font object.
"""
def set_font_width(doc, xref, width):
df = doc.xref_get_key(xref, "DescendantFonts")
if df[0] != "array":
return False
df_xref = int(df[1][1:-1].replace("0 R",""))
W = doc.xref_get_key(df_xref, "W")
if W[1] != "null":
doc.xref_set_key(df_xref, "W", "null")
doc.xref_set_key(df_xref, "DW", str(width))
return True
if not font.flags["mono"]: # font not flagged as monospaced
return None
doc = page.parent # the document
fontlist = page.get_fonts() # list of fonts on page
xrefs = [ # list of objects referring to font
f[0]
for f in fontlist
if (f[3] == font.name and f[4].startswith("F") and f[5].startswith("Identity"))
]
if xrefs == []: # our font does not occur
return
xrefs = set(xrefs) # drop any double counts
maxadv = max([font.glyph_advance(cp) for cp in font.valid_codepoints()[:3]])
width = int(round((maxadv * 1000)))
for xref in xrefs:
if not set_font_width(doc, xref, width):
print("Cannot set width for '%s' in xref %i" % (font.name, xref))
# Adobe Glyph List functions
import base64, gzip
_adobe_glyphs = {}
_adobe_unicodes = {}
def unicode_to_glyph_name(ch: int) -> str:
if _adobe_glyphs == {}:
for line in _get_glyph_text():
if line.startswith("#"):
continue
name, unc = line.split(";")
uncl = unc.split()
for unc in uncl:
c = int(unc[:4], base=16)
_adobe_glyphs[c] = name
return _adobe_glyphs.get(ch, ".notdef")
def glyph_name_to_unicode(name: str) -> int:
if _adobe_unicodes == {}:
for line in _get_glyph_text():
if line.startswith("#"):
continue
gname, unc = line.split(";")
c = int(unc[:4], base=16)
_adobe_unicodes[gname] = c
return _adobe_unicodes.get(name, 65533)
def adobe_glyph_names() -> tuple:
if _adobe_unicodes == {}:
for line in _get_glyph_text():
if line.startswith("#"):
continue
gname, unc = line.split(";")
c = int("0x" + unc[:4], base=16)
_adobe_unicodes[gname] = c
return tuple(_adobe_unicodes.keys())
def adobe_glyph_unicodes() -> tuple:
if _adobe_unicodes == {}:
for line in _get_glyph_text():
if line.startswith("#"):
continue
gname, unc = line.split(";")
c = int("0x" + unc[:4], base=16)
_adobe_unicodes[gname] = c
return tuple(_adobe_unicodes.values())
def _get_glyph_text() -> bytes:
return gzip.decompress(base64.b64decode(
b'H4sIABmRaF8C/7W9SZfjRpI1useviPP15utzqroJgBjYWhEkKGWVlKnOoapVO0YQEYSCJE'
b'IcMhT569+9Ppibg8xevHdeSpmEXfPBfDZ3N3f/t7u//r//k/zb3WJ4eTv2T9vzXTaZZH/N'
b'Junsbr4Z7ru7/7s9n1/+6z//8/X19T/WRP7jYdj/57//R/Jv8Pax2/Sn87G/v5z74XC3Pm'
b'zuLqfurj/cnYbL8aEzyH1/WB/f7h6H4/70l7vX/ry9G47wzK/hcr7bD5v+sX9YM4i/3K2P'
b'3d1Ld9z353O3uXs5Dl/7DT7O2/UZ/3Tw9zjsdsNrf3i6exgOm57eTsbbvjv/1w2xTnfDo5'
b'fnYdjA3eV0vjt25zXkRJB36/vhKwN+kEw4DOf+ofsLuP3pboewGISO7bAxPkUU+EaUD7t1'
b'v++O/3FTCESmcsILgQRuLhDs/w857lz6NsPDZd8dzmtfSP85HO8GcI53+/W5O/br3QkeJa'
b'9NERmPKgE2Ue+73vgj97Ded5TH1pPDEFCT4/35RFFtAMORMezXb3dwiioCsYe77rABjjCO'
b'jHs/nLs7mx3wuYFYX+HsEQyTfHg/DY/nVxa0rzmnl+6BVQfeegTyemSlOdjqczqJ0J9/ev'
b'fp7tOH1ed/zj+2d/j+9eOHf7xbtsu75jcw27vFh19/+/jux58+3/304edl+/HT3fz9kq3i'
b'w/vPH981Xz5/APR/5p/g9/+Qhb+/3bX/8+vH9tOnuw8f79798uvP7xAcwv84f//5XfvpL/'
b'D97v3i5y/Ld+9//Msdgrh7/+Hz3c/vfnn3GQ4/f/iLifja492HFbz+0n5c/ARg3rz7+d3n'
b'30ycq3ef3zO+FSKc3/06//j53eLLz/OPd79++fjrh0/tHRIHr8t3nxY/z9/90i7/AxIg1r'
b'v2H+37z3effpr//PPN1CIF47Q2LUSdNz+3NjakdvnuY7v4/BcEGb4WyEPI+DMT++nXdvEO'
b'n8iWFomaf/ztL8wZhPqp/e8vcAbm3XL+y/xHpPH/xlnDejXKHJTQ4svH9hdK/mF19+lL8+'
b'nzu89fPrd3P374sDSZ/qn9+I93i/bTD/D+8wcWxOruy6f2L4jl89xEjkCQaZ9+4Hfz5dM7'
b'k33v3n9uP3788uvndx/e/zu8/vThn8ggSDqH56XJ6Q/vTZKRVx8+/sZgmRemIP5y98+fWu'
b'Ao8vc+z+bMjE/Iu8Vn7RBxIis/q7TevW9//Pndj+37RWuz/AND+ue7T+2/o+zefaKTdzbq'
b'f84R7xeTdJYYJLOf7z4xq11N/osp2bt3q7v58h/vKLxzjtrw6Z2rOSbzFj+5rEd7+P84UL'
b'xH8/6vO/lj2/6Pu7eX7d3P6C3Y2tb3u+7ua3dkA/yvu+w/JqyV6GeUt0/dy7nb36MjySZ/'
b'MUMO3Hz5+LNycsdx54SB5wmN/XJvRh0z/vz1/PaCf4Zhd/rP9dPur/j7eDDtfIV+dX3+r7'
b'vz63B36vb9w7AbDn/ddLseown7kr7bbU4YIhD6/03//e7JiM0O669/vbyg1/hPdKLd8WGN'
b'PmnXoSs52h5200OGk/WW/fvdl0NvhpHTw3q3Pt59Xe8uCOARA8ydCcX433Z/rjfonfbrnf'
b'hP5j9MJtM0mbf4XZT4XT9czt0Pk3S1ALFfPxyHA6g2A3WCz90Pq6qFO+dsskjdtzAB3B+7'
b'rwwDeWi/reu0nbcOeMBostv1Dz9MpsuJwzbD+b5DcuGuKR32dFx/pcfGO9oOw7MZlAj64M'
b'/9bmOAaTJ/WFuJF0t898eHXfdDNmV4JC77x133J8XONCDiTTWq5JkvNMMLNY9C1ZLNa82R'
b'rIki9ULP50AZ/6pczOyn92DSE3IqRSZs7nc2+gmqKMi+O3an/sQkTQOpszcLsBTnsg2gSE'
b'f/KskTQ4YaANrFPFn4b/ELIEo/Iu2jQkbg/QEtEJXe1Y6MtWP3sl3/MMlnqf08D4cBaclr'
b'5KzEzHTuyXhZPyCXVhkcD0/DoXsmEwEfoWVQqsJ+Sg2eW9qniOGQFqHh3n+XCNMWCMLJ3b'
b'c4BPB2vz5CYenXkKjI06Rhu8mSJlSxKmmQX+uHB6g1jC0ztEQ+TRqdISmC6A46TLiH/sfM'
b'wBczE0mo4WrXHzoJpUyaKCvglLnpJC1XiEWSBN55eIHcDChLFpQ4TxZrHWkL2mUXwl6Yto'
b'N6OLefEmyRLHy7mizwDT1yt1szryqhfCOa1AJJBtKVZFRtCd8WU3pATvFrbr5cHlo6Dome'
b'tzoF0xmAbn3/vF2fgKgcbhbkKCCrCKBYETp0uZt+2siJ5pSGc92+kOVgbLVIOREE/rw+jc'
b'JfNGSxGWBysYMmOzxrCU3qelSBOUV1VQCf456kXEGaqB4gykGJUKTJQupBnixZ9NNk+S+2'
b'ihS/0kkCjOoD6ccjhCO3niVLKfYW367Y0xY90TIU6MwSVkRfVdMM6HFYsxzpPGobc0NLrV'
b'4ky6htQIoOA9rLmWTeIupuh6aRZaij5vPp2LH15zO49PmEMH1niBrcCCWd60KgH00/Bmgp'
b'kM8t9NzL/mm930scS/j7XYuHlr2MGiXkiwoDQvnESoFVyfKEarx1uSGFA7ehkULobywiRP'
b'BNiqgAcbOCo9MFRwtGp1GVn6wSDuzTImllwJ65b2mcAPyAjZxvfcTpHN+2xC0bZboApKt6'
b'joBDPZhbIgyyEeD7B7Sx9kZ1qTWqKgeUkvZ66MUI1N4eejGytzeG3kgUP/QumFyVWyD1+E'
b'pSja9NICVYYqbrSkvzJV2Xo0WhQfIedV+EsGU0rd23hAogyuUKtNZ7kBjOxTEPBT9LS/Cv'
b'BlfE32OqDgVzo+JFfWt3uqkhATv4OEhYCFtGXrRhR/jCY7Is4kuCVWavQ0QdiVoDqoiute'
b'kS9K0eFjpDy3E8nc75EdVjKGbtgVmg+1KkWtQAVp/hpaPQM1SNl1O/YwryWeEJUS3gUkeb'
b'wTnzDLP+DdtgG0jtClLrXh86SHu6mQoIb1r5HM1KWjmksEN7xQ9VsjVpEQ1ezvA7gUqMD+'
b'97RcpruAv3Le0G8V2Oww/ZBDpq+40xQxPBh2/G6D1BqRSiKq7YJ5TJKjTdJlnpDjptk1U0'
b'phVwrbvkabJy/S5Ut1UPnyELqgwIovM1Cm6jCoGgMDERdp6sJJ/K5EeKViU/Nqc/Lutj90'
b'OeYwD8UVS6Kb7RNzMrc/sZhqsZmYenfh3EnCc/StfWJj9KniAe0WFSKFE/hpxYWEK0k5TA'
b'wIh806Z72+hRd37UjZ50NJBBxu16o3UD+N1iHrjZ7LpRfab42+5KJ5gZH5eX8+WomxFq+Y'
b'++BBALJnWqVgGIRywArlFjJgefUXkgf/142NpPKQ84le/KfdtYs1kD2gjLDJ0mP7Hg6uSn'
b'tEb8P2TFYmW+p/xGo+B3kfK7SX7CQF4ZPE1++lUKGh3sT+tbAx3G5J/WN5WyDIzj5tQ/ae'
b'cZYrMDKqraT6b8fWshK2gxGcINBb+0hBQ8uuifpPuHY4SlmwhqwU+qg6frKFcRttbIphPQ'
b'R9WCwJesxfcF85bjZb9bX84siFWEiBYBh98kv1AF3jHTZ8k7PUvMVsm7v0F+TCjefdF4m7'
b'wTJWDpvmXIAeBbSrZI3on2gcBCFrWWCAN8BEhYRFXlK5N3elStQapRdRVIP8hQ0huaNirZ'
b'u6sBmN5NW8wn5kvaoqNFjZgn77qrpQeIFrXXInn3eFw/o62hZ8IU7Z2M0Qv3LREDiNQOJK'
b'vXQZEej8mQoT9th+NZO0TxyYCL+ukInW4UZFS14AO1SrX3Jnk36ByH4DIyMjMHO/jMzJfq'
b'MEsDhNLI0VCJyIAEUiopfEt7xzj2zk2XU9T0d9GQxPrzbdufT9GgMPWgrwuaWSZ/Y02eJ3'
b'+L5nZp8rdQ+VaWkPaJucrfok6uTv42mog1yd+ijEP4kpx58ndG2SR/V0NNkfz976E/WiZ/'
b'X99DZ3/uoxF+AtjV1Nx8q8JEqDd7qhkZYwUmB/byYoqG7OuuvwX63cnibJH8XQa0Gt8yoO'
b'UlKJ9v0JT/Ho9fZKuWgX7i7/FYPwUQLU2skr9vdTKh0/19q9UBhOgHI0gSjz0QU8+WUGx/'
b'jwoFJTAgF5SXemIhmYEhH066cZUEfEE2yc8syEXyM3s9aIU//4yuEtXlZ6815DN87+83Jq'
b'fh3OdavsR3yDVyJNdSS8STlByRjPISnlz/szJfgWNp8VoGUoZiqH8/969RViOG35kMcOJs'
b'RBqibJwnP0fZCI9+gol2Y79l3IBnya9F8gvza5n8oip+mfxihVqVUD7tt0yJVwRchW+TX0'
b'ImZckvekjEGPeLSjJ0nV+iejSdJr9EMkMGEQvfVHGMioqq/cuFhbVI3lPWNnlvynaevPdl'
b'Os2T974coS++D+WIye77IGJuibgc0dG8j8uRnqKkTA0tHsrkPSv4rnuk69kyeY+yEBW2Tt'
b'6bQmvwGxUa4tGFBv3ofZQBSNjwqnMI8UiOgOmXJJep+5Y5AQCTQ8vkA3NolXzARD8tMvxK'
b'qc+TD37AX+buWwIAACXpGM1y0I048Nbwi+C8ioAS+eBzH7J9YK7Bw8aPCTPIE8pgaglRG5'
b'YR4KsW6t2HmysAy1oz/LxzmWlUD8Vx8JLgCPXzKWgAH3T/jXRhfPKVrJgYUlSXBcigutDv'
b'rXxSsEROTCkjCMiMz1JUDQCnajBhkaqxAhD1zwXoPeodVNIPkQ7Skj6yUDBImU/J3LmllR'
b'BtZiHJ0IWlo6x0IfrsahmsVlVtHvWMEcFdKTzwLroNeugP8WICa2u8mMDA9t3T2iWOn7rb'
b'd1w/LmCKbejjcDnoalzNLX7uzzutF1ULh3v1BrV031vx8pkQwqZz3VrhQjV6CCNKFtuGJc'
b'J+CXy7FQn0rh9c3zxhZTbfMqVtHSDFTRe+D0CUduDXzrX6WJH2vUThvn0GM8sNoOYxU+9B'
b'4iuSX+EZWf+rFMw0+TU0X/B111iUya+R0rwCHaldcwA3p7hzeLXr2/ywCsMccRkI8fevR1'
b'3P8+RXnf9Qtn49Gac1P3QmkOOSg+//ZnLS5L9DEsrkv6OQwBT3afKR7rPkY6R7LkD7bmCa'
b'fPS9XVHjW8Ya5MXHEEsFIhpVyFb9RzoBqXOyNrRvkMU8kKIiFJAj1s4QiJqjgL0dmCdIRt'
b'jbKlcLknFrTJFEPRoVbfIxyhXwJVf8tw8E/ut0hJ0uLx2tXMBryuQTczFPPq24YzeZYHqP'
b'/hJU5qh0Sir31ITU1FM1qcJRufFXOiozVOV5JpTa+zO8mXdJnoncxM4YUpElI+VdlimozL'
b'ssycu8SxQaKC81OltQXuqS6cu81IUJxUtdVKS81MWSlJe6oJyZl7poQOXisiUlLlekxOWc'
b'lJe6YPqmIvWMlJe6pNRTL3XJtE+91IWhvNQlZZl6qUtKPfWylCyHqZelNPF5WUrmxFRkYe'
b'yFl6Wgv0JykPlZSA4yzwrJQaa9EFmQPmll/ls3EYqw3r/0vsvHAPTJN8XSf0ceSgdKS0BB'
b'qAaLzH7YvvITvb/51OsBtYVubaNDutDSa0vIXJTlGzX9jDU6kmtiaN/2WOU8GTmDt7gzhf'
b'jR+jzSF2+AVgT05AxBbB9iCIUVzdcQ+zZy0SB5236vlk6Rov7JrLTOUYD9nyIAqkHUa4A7'
b'PJ7Ha3DwLn0JXJwZlszn5slndhbT5POaSiyGgM92wQ6p+yzFCzQUHDLsc8j/mSVirR49/+'
b'e4/6WnKHfnhpZCWCSfow1iOL+5+Tunw1AEiL07n6KNW8i6dbv3NT7d0LbgJ/WxCRQp8ymD'
b'Lmlkh4SJqNWgXJIfzwyh4n/WvTemB5+jcoAIesERk97PUEgee6OwNwtDnXrW1npqiPPrQC'
b'Gr5POxg47h1WhiCDtKH5Sxz6d4Z7EB4gsY4b12O7XkD+brIFSafGFxF8kXmY7M3bfkBwA/'
b'uUCxfJHJRY5vKfa5JcJEotGA1INSoxID3aoUIWCl6aPufNEj9RSk0vQXgfQ+llXAJOYsYJ'
b'KCmcKU2cAkwC7WlMm5NtUpAihpoTxKk4e0MnuYuW9xC0Cr9JiefPGThJX99Gofpn9fRpME'
b'iqknCVB0v4wnCegqvkSThBZ0PElg9mpIZwTy7EpTgYxab6wgmGQIGvGX6zXS1oNK1a3oUj'
b'cRZKWo7Cwr2SacF55I2T8Jy+QM03p6298PO+nAcnEgi6lN6jG9ntqMwRuBTb2bwIuEkPkI'
b'0mhNnVI0/i/jheQJMd8ikR7MG9bcJdb9WBvga+MTlJGfv2MY+hLNJCoPSFWfJv9goy6Tf4'
b'T22ST/UHUHU5N/RBOFDHS02gEHrsdpwIuKCuFG2yd18g9JHHi+rmFK90+KUSX/9KLWWfLP'
b'INLCEjJSQ+5/qipSk1QjBKZq/1RJqOvkn77q15Pkn5GIiFNEqpL/oRh18j8h6mXyPzqmBU'
b'gd0zz5n2ikz+Ges5tZm/xPFA8ClXjq5DfGM0t+k6506b6lwRPQpY6x5bcgVWuJkCFl8luo'
b'sSljuOpuVsC06K2hpY+YJr9hHqA714bI5Va3h+B9hqLl/+aLP7efvktZQSi9wzEtQOu6Xo'
b'GOhkfonL9FuYYsklzDt68wFOByuu+fdAbNHXbLYGJB3q4/n3e6LkNREfiWrzr5F8tpnvwr'
b'Mq8qQfsRZ5aIGVa1dN8y/K8ASJE5whVZ2s4myb/sonPVmC9ReBztS2aWJf+KWmAF+ub2RE'
b'3GDa23BW7VGoi+7XRa5gTGO2qLlKiO0vi7Gafl3Ih0kfxLazqzafKvqGgRsxQtv/2uVFMk'
b'tEmEvrFe33cYbXZoTzM06bVvLC1Zm+4rnM0mxJ8uv6+P6zPczWtLH/eXZ65RzA1/v0Z3qc'
b'C8BXi8yML5JAf9dYD2QwU4RNq0Gncx5hGooqbre2Zlb87D7NfHZ121VxFXBYhhVScUyb8f'
b'Xob98Dj8kNN+ay2G2Ln7FkvnlQN0vqcO03ZLlcPEENs7igySfPBipgJRZAsZiZO6vJxYQl'
b'Q4TEXWNwyxC41qq+SlZoghdqXRyBB5pjlict0kvkZAczefJoKH/T2qelpZyFKT1FFDRLoS'
b'KJx3LtkMXCRBYzUABm0XwJQ+Qi7nyAG9pgzuZrN+VnWsIuTqKPJB6aFQ9G7OTfMAB70Rgu'
b'iMSw0ZlidBmxaBWh4WF5G73fNw7FDvcq7srrvgAZE89v2EO/g/QOzCkvVsmtL4aGrIdII+'
b'yFqqe7K2xs6enFlFwJHZxFrJeDK11p+ezOyevCdzu7ftyantXjxZ2A7Ok6XdhPdkZbfaPV'
b'nbzVpPzqwpnCPzibVj82RqzdY8mdmNAk/mdg3Uk1NrU+bJwhqLebK000xPVnYm4snaWgZ6'
b'cma3Wh05ndiJmCdTa9LsycxO/T2Z22m/J6fWLsaThR2kPVnaGbsnK2vw5snaGo94cmZtTB'
b'xZTKwxkidTayDrycxaH3kyt1aWnpxao1VPFtZaxJOlHeg9Wdk9fk/WdlPUkzO73ebIcmKn'
b'qJ5M7Ua0JzOrLnsyp8WNSFVOSYpUZeEarSMpVS4FWlKqXNJbUqpc0ltSqlxCrihVLiFXlK'
b'qQoCpKlUvyK+ZVLsmvmFe5JL8yUknyKyOVJL8yUknyKyOVJL8yUkn51kYqyY2aUuVSvjWl'
b'mkrya0o1FZlrSjWV5NeUairJrynVVJJfU6qpJL+mVFNJb02pppLeGaWaSnpnlGoq6Z0ZqS'
b'S9MyOVpHdmpJL0zoxUkt6ZkUrSOzNSSXpnlGomCZxRqsInEADJXEhTglMhKVVRCEmpilJI'
b'SlVUQlKqohaSUhUzISlVMReSUhWNkEYqn8A0NVL5FKWmdU9WQpZ2DuDJyppoerK2xjmORM'
b'ai8ovMJmMLCcpkbCnJNxlbBZIRVT75NbpNBFUJaUL26a2NVEub3gy5nE1cg8y5MDxx4mO4'
b'JWHLrqhyVs6ynAsJ4UvXrkGyVpTlRMicZCrklGQmZEEyF7IkORWyIlkIyYjKUsgZycqRU9'
b'aKsqyFNELOhKQYbnAhyZDdeEGSQWVeyCmLsswyIRlUlgvJBGZTIRlyVgjJBGalkExgJkKm'
b'TGAmQnKYLjMRksN0mc2FNFKJzJmRaiGkkWoppJGqFdJIJQnkMF3mEyEpVS7p5TBd5pJeDt'
b'NlLunlMF3mkl4O02Uu6eUwXeaSXg7TZS7p5TBd5pJeDtNlLunNjVSSXo6t5VSE5NhaTkVI'
b'jq3lVITk2FpORUiOreVUhGTrK6ciJOt5ORUh2dzKqUjFwbScilSFEUOkKowYUgqFEUNKoT'
b'BiSCkURgwphcKIIaXAwbQsJIEcTMtCEsjBtCwkgZURw+dkwZ6qnE+FZFBVKySDqkshGdSs'
b'FpIJnHsxClOfq5mQTFEtjk19nqVCMkXNXEgGtfRCFqYElz6fUQ+ohXrHJUuhaLyQJRNYLH'
b'yRoZ2DXE6EpONlKmRJMhOyIhn8MqjlVMgZSRGDWVcsSyFTkpWQGclayJzkTEgjlSShMlI1'
b'QhqpFkIaqZZCGqkkvZWRymd7ySG+aCW97EWLVtLLIb5oJb0c4otW0sshvmglvRzii1bSyy'
b'G+aCW9HOKLVtLL/rloJb0c4otW0jszUkl60T+vmiyQBUmf/Ap97KqZBpJc6UUrdm7FaiIk'
b'xVilQlKMlU9ghQ5q1Ug3UnGYKJqpkExvE7imIpVCMqJGxOAwUTS1kIyoqYRkehsvVc1hom'
b'gyIVkKTSokS6HJhaRUi+CYUi2CYyPGTEgjhq8bdW7i9XWjnpqIVkIyooWXasZONXN+yzRD'
b'B5WlTicHiSLLUjdBK9McXVCWujlXmRY04p9kCyGnJJdCFiRbR7LRYSh3jvO0NCOsczydcS'
b'qUUWa/kcHqqldniiRanAG57Y/rp/Vh/UPOk7jraNoPifuwMsL5Sa+XRiBU76bYnKrGR5UR'
b'dK9iNp5V1MbDeF2IXTpvUlnfMwwz0PSHRyA7h61ogQ4M/517jTZE990mAhcER7ZUTNKNlS'
b'aqVP14pWkagSoxdP28PuOvybd5Fsjtevf42m/O2x9WKy5ByDoAR5Fd9+i6THxJMqldgN6s'
b'n7rT1iwGvrJpWVdx6uvWgNv1/tvalFIIJB9xRh6ngW0WM4LHYsQZeawt24olwu/WyGyR1a'
b'VtzzWYkVjZiDMK3bOfT5fjWnxxLA9w7GU10bxxRVjlmjuqECubCS8oqpDPmc3SP7hIeQqo'
b'SdHLFg2Vfdxu1/1xWe9+yDJqDu64PXsdfdx+DlY4bg+mXm6lHrR/6Y6n9WHzAxdWAqmdTR'
b'TuV2eN22BPjyw7qFbIHD48aWBK4Hm7PjxvL+ftGhWWRlHAuHaYcVWFn/fH9cNzdza2uJgt'
b'1FeoN5lHxnEiq7jmCiN6ml3DytfUxWSiyPLMuba+QRuZuOxsrDDRgg/DGY575m2NNnG4bN'
b'bns1/Eo2J1uJy+sjTDYm0A/VpfQHS/BzRcdoACfVmj2ML684TIsTv8kPFAwPploFgv0Uo9'
b's1Bwu0rJ/v7lBbm6qlcrfh6H9cO2OyGXqSSS/lPqTa2B4Yi+74nFwWQZnJ1ht3sT9xDyuO'
b'7UQiLbPpEAoJ8/PiAnuRJocpWdj9nbTNvZnJi50YF6RnSjQ2NpOXmNqnk8Dq/3w5n1fTa1'
b'5GZ92m6GV9oeUI/xkC1NXmQhkCtRXm8i2OWFgAt5c79zgS+ngriwl7kgLujlRBAf8jITyA'
b'S89AHbMGZ5IF0gs1mAfChUqD32uu2RGRDRuUNZb4i79ecioAzQoVlATZgOzgN8eXGYS+cW'
b'Jf2t+xM1hPocES/fJJBIlUq2Q9x+TMYrWARHB3r0qeH6gsclNQ6TFGeKjgJdKQYE//r2Q1'
b'bNWgUyKierT4zBJSqXmWfeCmSrxFQQqREuH02hzVJPbEyhFYG8PzHIeS0ISuJ+PQJ9zpUa'
b'GB5dHVhIcJL4yiMis0OMTmAKBWGdHvrebm5wr7HVQLRf5jjeTLjStHZogzj2LzRg4+zQEv'
b'5Yhmnx9gio0rxSh2mtYoxp1YLLJife8HZ65mgyF2q9456JjKRUDT3nBoY+B60yS0No0WAU'
b'gnVjUcuFIAuh0zYKo5ivrkq2pdPb/uU8mCFAdWZoIWcesEAV9/nHPuUcGYaTKfGgjwo5Bs'
b'5F6aFTkmrAI9vroeRptdPSQe0kvUNQ5y33B0OgnF5ervRRdPCXW9pihHttMQK1tgjGV2rk'
b'Wz9Icdk4ugqH2frWH9wM8o0KD4sxqCMTg4oWBlf33KPFjxoNoYDcYyT2RvKFIqOaTNxJkv'
b'FbyTq3tOSA4auKWk1In51aAb3gXivCS3KPbBz0doxaBRBVZhiD78N2ZprcRxeb5IaW8Qlu'
b'O+pyp/7PcwcnWyoKGGXLEoF2D+sLO4ospzO9RYhQaRriNdGaZKxLohMGNtYhZ8ajSvOM9E'
b'iXRM9qwG4/8r6YrYRzGnYY1DfCmhgZDsMQT2oWaJH3nc5HxqjtMljQ3dmur9xbU4LGQOuR'
b'FRQTdLYzCc4h0kCGiYUBg0JvSGjZobahJt9vdb1akvY1xhC6yjgg1BkC9nh7gZLsdVaS1g'
b'klvUMurHcPKDVzIh551B82eq4Ine6+V+YCTMEONdtXIJ6SNwBKCHVuQ6R0CAaHl6E/nKHv'
b'QEF1SjBn+YbNEcSzzW93pOfpNVd5xqzfscF5uKAYY106/d/4WqtuvuPO69dp+r850CH55P'
b'CWO8aipEU/G3jGo2ZmlnnsHs4em7vAjNvrzGnmN9g6a13Om57cFZm5u8Ch/Q7uH9kpZKXP'
b'geDMZd3pjG4kK9nySZrb98bpmireVbqCRyehEUeLOR270EyTLYdn9E0Zs09fU1SBHlBTsw'
b'JT4/toigdfwz1XNXrXP6ZI9aCrP7J20NUftMw70Gr+CLM8RIuy7oyWgnmrIey5yUnVBPL+'
b'TH4egH2/IZIpRPfCyqsfajV2fqHnNAC6klUWtrUTYiwVbeVoFeIE0Y4iSTRDRFko0MqiES'
b'1MnehGh8Gu0YAVZ6Ihq++tNBQNipF/E3fbJlGDRCTLCLGxNBFmC2weYVE8cRA2keju3frU'
b'sk7CVRvW8iVrLeQMaUpLycKWcriKWc4OJ43RzXCBwm55JXn95imKbu6wGzHk5GECcbCj/B'
b'yyiNlYjdzWuiCchiu5UEEvuh3A40W3A9KY/p251Jm5bxM/R3au9VtoQPCYtx+pss4Mdure'
b'TJfcJg/Uh/LkQVsKloDVOIY58YPc01fh2yuNxLXSaOmgNJLehWPeNcjDhoP3YaP00jrVuM'
b'v9icb8GkXkUC9TkPFysv0Lj0M+IMbh0a4lO0uwbFHZT11mCwu5KmIo9GZP3bGjEg3/Dfzr'
b'pVskQe6kW+JbriLEFOlhfBXhDJDoapklwr2D5F6OO472iMRdQdiYr3AFIenQucGdRNjUnn'
b'BpgQDGE5dV+dU/cXGHeZBb+vDoK9lyZRDdvtqJgYbd5nR+49JM5YLRdRNuotM/0PAetMIz'
b'a0j72mEIXT0cEOoHAZ27U9C3b1NckvPwzLkHJtxpbsjAn1YE/vfLFVeRE82xnm+YCxdkaC'
b'vpykR8+3LFBVnfv1yRWUUDa1bDbd9deEbKVA6/LpVVgWMGN2Gkwhj5KGeeEZbL5x6Kw2B1'
b'2w4ImlM4M8hO5h7xQG2BPjhxnobOA0yku/EQrhnPVSpKh4/S4OBxClwoQX4HjKR36GUUKM'
b'QRXbZx3/vL7ty/7N7Q2c0qh6FxgZo56mV34VrjrPD0AL1pZ+pWjs7dobxTnWMalw+MysMe'
b'daKYsnQo3DTRTTxblMnofJBrqkuFu74HjW3XUXkzDZk6/Xr3tcM8iOPAIrPQhnfW7whMLM'
b'Bp0tEiqUXkMBUx1Nbd5Z4TPvt1uvRnJ6yG3DIPbUoe9g/omUOXM0eTjHQ1+HJr6soRpNHH'
b'JdgdD+ZoywQjn/nc88TX+vjGbfJUIAk2dc64AqCciH5TWNqqmlTome12xXCZjnkOp1Dmsj'
b'buEdqTedxIceNLriBTkA4vEn2Ib1UuvEM/H574wNQS99JCqodtUwtFy0LOp78NT4szjVlu'
b'ndyFK9ngkqS75MxCds1HhxgxXHgNsRd0XZxDUJrD0/HCdJp1c75NMFyOnLA8Hc36E1Qo82'
b'DBAILG5o6YL3h5ETQqRzct78ChZuBoHsZmk7XkYs5rVNJA88Q7R09LLhcp2WmgM9JZoHPS'
b'eaCnpKdCm9irldA/89JRKhCWbnnhDNQeT77nAf1JIfQHngadSHDtJ15VzKHJ0Z952XJaBZ'
b'pnbUJmrHidoSlaSzLtqZA/GlLS+pOJS2T52fide/L9nPmaimgfjWcpg0+8b20i6fzEq1cm'
b'gWvTIdn2ycop2frpi0mHRPbpN1MqUohfTGQS+j9MaMwF9/QGFYtZIE/rw4m6voZQKR+pXR'
b'BDrRtN700ejeBoaTa75utdsTRmy2ba8gYehZvfcKADNvG+DEd7vsF3aqZCBdWL5Q9Pz08B'
b'QtbJJBTFcLx863p7FyZChALQnalWcGkGnqHpvXELM6ONvqGMOk4F/HJEIA9vzGDUwrejuV'
b'Ob+ZiSWrEvX9H0CMS9ZxmHj45VJNwaLafJJlLiSavFqBLkJtgIGNItTZnveImvaYmNl/ig'
b'RAEd2wtMErdyZsxAomUzjzxxDWSSTdy32bmZZClJtSJWGjosiJFW05+S3tX0x0S8CyuVFG'
b'5nl/ty+xlW9CIgrOk5eItA7f628XxnLGVGnLDyd8U/dU88Nek46Zgz8un5AXVAf+z/EFdT'
b'BY4C8CxoB3sBZwocuXesOH2VAkfuHctu7Qtaa3Tkw/Mu9xflo9HoyIfjxTlXKnDk3rO2ps'
b'o6cKLAkXvHYqfUCVgocOTesOImMJ8D00P/dGUBbQbisfP6MNpCmi4CJ8IOvApuZprn8SnI'
b'Pa8sYPrFCMRM4+XQcZdFjvKYQX5aQ+r7nb8/lfWIy2/XRgrzWwy9KrQcO5DetbnJ0X5b4+'
b'LIecP10or1rvZv0XN5RG1Sc1vb54tJ05NPUymUU5RXBLSOsiCAGLnayKNBlaLd8ovJGLMx'
b'GzATzsux33ujBJNJPmFcf8k4OiqMnpWGNWHC1c4MWtl9GBzQImShAFGpy+vR/MOqQG6J0W'
b'3kRP3l9XAedeOG9h23IXQP6oDQhRog9JGYtW3GFb2pIfpmIxP3Ajm6ifYxskSxM0vpWD0S'
b'oiWid6YaQ8tiMOqbfQrm1L2szdJU2GVtrni06zFjmmOqvSrUpo6bOFwQQZPvtn1oOktDh9'
b'EDFUPfQoJS0XtHC7LROYjZTeNosbspCdg9pKn9lCsDa8Z1GPbIVsiLn8sJXcHhsrfrbiEr'
b'V8j/jvdkZxjr40yuEpXHhtBZ7ICQwwTcZhE+MR6/nblD5E/rFyPMnQacJrLXwxMFjogmgS'
b'i6cOZvXifx1RNoklUS3TzhWvpUUNc8gk9pzAGK5NSFxNh1qZA+nwc3OYfaven5JhtEW1Xu'
b'm3P5zDL4wpLdxs0y6NGb6D7EAmE9n7ZmUayYwUO0P4HqEJYqobFtwj30aEPRHBhJPchmBg'
b'guomzWfokE3cKAmuW3MsjXCURb01sZC9I7M82fMA/Nt55I5g6LZpLeoVquE89iCuBD1tNF'
b'Ojo8UUdF9R7U3iBrd1h4zJazQLryrBLfgl2J5wEYFKISt2IkGGxOvDgtzVNP/c4rUluh7G'
b'KZq80mQ8/OwGJRkOCavCzzoHMyK/Fvw8YqNMYSO8ZEvzOc1wMS8qyP2LaCurUCRCOqPLzo'
b'HEMSzuveLNMii8LSPOTQS/MctvTSPCU3r2kgT75ZzYCNnpQcTS5J2CXgOZ3ffmcjJUdXYz'
b'qNVj+LVcIGARE6OWo+w/eReciTJJ1abIdbveS6SDq5ox7+7fq6X29fekCvtQt4ZchRXHG0'
b'NYfhuhbV4Hv0uAeD1UutTM3D9i2+Z6GuAMrgObVEOM0914C8+LHSqIyxM43q2zErzZAXP1'
b'KNRtde5pojb3tQelVCEFUfuwbX5zGk02eskTPuSY8q6aInPSwtR+Mhf6f3+hFOd2WHAz/6'
b'3Q/0XJ1YuNf4VsUK/1H2w2u0No/y0YZX8B2dwYfckY07gnOrBnltP8MI74BQKdvWIlK0jD'
b'0AbkeLSw52jSGrZql14HKxdAF0mEj7MKpUMN+2MdoIxAa+YXufWUzlhRdH5aSPYIs+4yoh'
b'XFT/th0uyJfMQzS1sdY3HFMbi2KwGpD/L9verRzkWeZSKl1+NqldGNECqcNUh+/z1Seucp'
b'FIyuqVAE59Wjkv/m6sykUu/V02qZwTbwBNcnwWgL5u3DqCzNVmeHUgI+N+1MHn4YBc1JcO'
b'GNCf/AehX4nJkbBdt7frlFArOvNkTKgrc4dIRrQekDLOHCIJp59d/8JGl9Go3FMyscky1o'
b'KgA+SekLdoKo/IWzTIAP0WTY6+db8xygiXK+23njmhgkZ6Bf2/cAA4je/gaMg5v506kwVw'
b'F1myQzY9YmA21x18vLn71vFmxG5dNEfH5g2chh86CkY5ehSH0PhOeRTOwSbHPGHZhRdy0M'
b'qGUMKIyN5OmzFp/HzYDSe7WDa3QHgzBoN+DInboo0ZXiFGBvjKMJ/g21+0hVl+F99qhUmC'
b'NbZEP+U+o2bnMNGpSkerBrMg1H/FvP3AdGclivWo8w5+dC5PIZFOXB1I7Qox671IjuK3n/'
b'xBBnLpLatzfjh9oi5JDEffQUIrtfTVoG0cegF2w/DCq9nmBKkbnpWk7D2vDHArh+mWP8ai'
b'1VgGfTZG+xseX6BcSttCZtoZVsUPNRzVpKXU4Ms8VbRCXsqtL0v3LUM8cuaM2M/rxwH9jE'
b'wMOXYoPFpvCbwb0LVLP/9bIu6LVG/WAHkVqbtlB1sp2BeExrTeBPzPB7PSxwVT+637hoXD'
b'7JpqLiTNuyfcSgu03KnvwWhS4UE5P0MAUzXaDpgeEbMvO3dlf6reeFoZyla8mXGjH3yaEb'
b'AqdNrMk0dqqmXyKKsNLb7VUGBoBHDYdj1XhyYz0OetWoVrLRCtwjksWmtrkke9PlMnj0F1'
b'LJLH6MWpVfKobF7R2B4jbQjN6XFsBLvMiI1XyJc50dEKOTTVR730gNgxdlASHvt+fMRMZc'
b'Lfnh8I4HHHD3gyAITpHyPVBtqIg0SzyQSRQQ8y0xq080MBnex2GMeHP63JoCVpw2jNF036'
b'nteP9iCwp8Ia+hgLy+iBE5ZVAxYWkud2sThmKC8xWxZ753ZFN8JHvhx33+3tyWRPBWcOO1'
b'wO9nSyp4ILh7109giyI4LxuIP4ikxvzyEHOrgiejydzRVMqB7diToTpvmPPeS2Vlck4kfL'
b'GLRRy/PCfAUd09JKV24MEOrCVNE3NOW6NXyvKFvfVkeF7pMWSwNo7bdxSFB+LRLrvoXDgu'
b'prkVs6rhVRq7jWbTTUWkgruBYRta62pKi3C0977da6Fx3PxqqHauvAq7agTDtDu+DBMvMm'
b'Eb4jlQxtKBwhxFThcXgUexl2GsOjX/eBqvAIXXAv7CnZR3alvM474XPYLN+p+Qr5aGlVvn'
b'MDhPLNFX2rfJeG78vX+tbF6ZFQnBaJi3PqsFCcFrlVnFYiXZzWbVScFrq1BFoZji5o61YK'
b'2joIBd142he0dS8FbeXRBW0dxH3mUjDpNNMASa9ZWMzVERfQdtSaIZEomAjkuH7g3jFP9k'
b'xJHR449ucJTxFiKvukTeRI+gOFBb69tRzxcLZ5viIZL9NjaH3iod5owGlmU6LxgNPMGLI2'
b'vasMHSzvSGs1bgFaq3Ck7UuHTW4/dwjJKRCYMDlQ3cHfTgDF7x82iZ5DTJYg/VITkifqA2'
b'RRzyEi5DBMl5YIzyEijNFziHDvnkNMzVfggI72CuBSL2EUGWiV5ob0sOcOV3QIq2A4x45v'
b'ZjDkoAAuHC7IKnfI/vLHRu3CzpbEUVl5kpCXpq5II8A33nkeB9oGVggXRQzt162BY0r3FB'
b'ld1qT1M49VZhBXsQxb1wUHhMpgAH1/wNwCoxsEWote3SGwsvhY50F9+N5bkwVZ10+KMWE3'
b'3ppE/m/D5tTcUFphJGInfiXjVE8UIkC9uQAt8UlvLsxJa12a1brfdzt7A4v5DNpPBATVx8'
b'FBiwAQbzsg0N1wxvRBXq6QK0NbzzqdOfHK2JgDoF6/gDKnGO6s7ERjaqLG/L1mOE/pLZ5u'
b'x5EIXtRsnl7DKso5Uh3e+ITbaBRFC9d7IOhVn/QeSANautOM38G0EI3syOsl7eJPlfjlSx'
b'Y1P/WyfpnojWLnwN+c6UhfjXJLhpszWwtEcjs/6jZNIh2NLjmUt57wXQWUIo0MR25vAF82'
b'Ho+GSPE/HGUJgcms8sBwIVSVQF9VfILKAgUkkEO0mIc+hUdSwdEbFgWScuEEYD/4syDzJk'
b'De5qux2Kk/PLlz5pN8FiC3OUo7zye9/dEw9ON6HzaY2Mu8hf3xWcL5O6b129uPrs7IiA0q'
b'UHV1v9fQyU177jwJJ0bpSN91a+lwoy5pddhxSXJkBpIRG/d689ygYf9nRXrUB86nAPuz2m'
b'WbJ9vIgmmlaL1MUtPhDrqkXs2ncLymRKRNLRBbqWTpnTFLCSw9K7bcheXGE2vLahXr2mNj'
b'udFFKKlgz+vTcRQeqlnEvQ7Spep0eb6MWAVznja9ZqJ65MoKM/Tqyd0pM+v4MgzmEoP79f'
b'HenJtvFh62p448vqBIoSbSs7L+ajJFm5udIiTLr5DHMRJs3zR6cJcd3OJRGLTi20zUie6K'
b'I3NqU9sFSO+voKy+gvLpFRQiiOCx0BHzSuqIG4vtWN7eq0kVbS7MipBsOkbyyRgJYWt0LL'
b'DmXcmrmbG44LhHnKtEb4NN0K7iN53RItSbzuhOgvZaWSK86VwkW/2mM/jRm865oSVkuO7s'
b'bW+8UOXMfaTCfkZ2/AoTGw6I3wXNZSpUUFuIbW90sHoVrCIpeo3xYbtG7W3VzCvNOb8O0v'
b'9h7rkdL5tZ7Dv3LTXzIuaOj4I3cyOG741HgtSaJxE2Bg2H6Iwr11OPApgplvhHNwI5OhRc'
b'6DUqBqpP4tWKjjryJRmXc3Rve14CPIjWyvw7XtQwwVHJ2rGSpSxFQXpPpf3Ur6Ch+Prucn'
b'2uqHH46PCMg8cncpYWDidyWguMTuTQmc5V9EvRCXVNRxnCaK2hK/Q+85lOFZGlmtgoIrRO'
b'B4zbuoOvmrnD4xYOMLrmH/kZ6X4oUH2mpcKgAR32xS0MsNlHJ5RJ6+RrOko+ctPZ7VIX4W'
b'c6U0RWKiLPFBFEd8A4+Q6+Sr7D4+QTPAzP24s3VMoomNvQ9zrzzEAPmnjhQgAUsG+xnWdq'
b'mHL4SLMysoJd/ZS0fop+ZuhvA482ObPLgpA7lclqOpxPL7x5ydxdwYIxN1fw0NRW5g3oPH'
b'VbQHHJPSjsIqNjtKT7Xl1klcN3dLC2UHRUfOgMoseFsuUyQlxmQeivXE9EOG8vW+508mpC'
b'+62tuzw/2ojxDkWpzz2gdspKh/EdrYzHXXrq07OkFxOgJb+VlrRK1KWEdZVoe42MpFucga'
b'C9vB+FcMOAVid9bHDTJvpdlKJMem3lAmH86qExRnIB5Vm9CpzH/tgFRpOoBUea3GJW0PmF'
b'x3yluWQLZx5xkCsqUIwpmsnNY5oSlhFqjorlPC8zRs2sZ7WC6hlxuO1/vuzMoRERo4rdHL'
b'm3EuTINdfkiCypRikzzxmjwp9CypcR/8+Hbse5ogQ9i/iP3GHFbNL7xqxVczHgHh54c4j4'
b'Lm/yJfIR+yhiZVFxbddfg8BZxIH+HbIhysieBxj9syMsgKiwduiOjkHO+oon8cUsFFmILy'
b'oU9kvCiRLGYf+B9uHCnsXsc8gSdJaaNYQqkEU18bDehyyJ0u0WnHOaSWiYx+9CgqNoMPI+'
b'SI2Z5jHrBVolaoRENovZJ24hBFHicJXpFVId5eSpe+A5JhFoFjN3jyJPlIzT8NB35zeJLx'
b'LW9nN8kjNGu6jSRfXgdB4enoWVxqzLJkQUVcjTJbTMOC72o191+1po9itXVKRAY9YwbIQT'
b'Nbpv3XFgolRtM1Um9G0q01ljAkNVGVaYkNuqxiAtAVeJMbKGoJSwFDUwjKzWFIQSKovDVS'
b'C9bVOmMG2KyjJRlpLI7KsnmKCiRvfZshw7jo9jpdTjI6XUwWOltLJwUEodMFJKgYp9I7JC'
b'2zeSpcwlQeqVYeR0ZNSJeq4HS7QJPdCxt5Hs5LeOyNIhJtJXhpkowSuzOmRnP35Wj+345r'
b'27E417E5II1DYkYPxOC2y0Q73+PU1uqujQ5ftgzAI/5ua5bIkc3V3ewgEL0GIgx6Hg+l3E'
b'PDH3dQ7Hm3d1FoY9euIKVS/Sw5EBB/RB3vwPXfbB7IHxfH+KJnXQL7WVkEIdDQrU/cBDBD'
b'zFkQbsHNP2CppCaC7Jw8EkAIo+ome0e35ZRhHPfbgVlUF89Rez8BYWkGLAvqTrr7zPqQu3'
b'OfX6ofgCIonhHJviYE2iZuZLve+4mEeIt45i9wDYbNhR+7X+xHYKAYrSjApw1JWVJX9l4p'
b'U7TNecMRaZeCHBp9N2rfd8IalsJRi+0mTRNXklQEU7U7A+UkDYvRPJjI8svtgjRzccwsFF'
b'q8CoL7eeS1slV20p15heQAb+bdufT5H5RuFBOaymmFXyO1XzefJ7dHdKClrt4i1A+i07fu'
b'sdO0uHDTvQ2tZ6kvzu9fUVv0Vfn1lCFqDQGf+OJno6df5MA3L5d3cMQ8qnWCXxBlYNutuH'
b'tdmFoUdXArYGvLoTcGXg8bo4pFQLTTNGsB2dSWuS36NdziVpn0GG0DnkgJBFBOKrWxAgWk'
b'3Oo/6/Rz0MCkYaBDJIzyKzhNeEolfByLA+bZ/7yPIyJRwkLEC6ATQnS3fjc9A3nyFsDMOm'
b'igE82mcXnpUtABpgZIbVJDcssAw4MlBjpMogyzi5slcz6HjvdkEwvttwCUjneGHokOGkda'
b'/BcMfmwVNguhdpFB0NQCUYLy+m15vbz/i+RlRzoG/dcDnsoQfsZbSqUmG8cNXqJaxj1dPA'
b'Iif4qYVxOq2hU8TcGbjH4dirDp55cdr2mzUm/EMop4mGUcF69kz2CunYzag3XTHvwjVZlF'
b'PvoxST5GrrxBTH9Q76KmGwLAYMtztjjnR8jnKWYX33kiI0o2e92N0mz9EFXjPSzmqD32K1'
b'gYnvc+h2UGSxkQbZSnGEGvIcm1dOCai9SZRiZJqh6Sg5kCK+8BM5cGWQvEJ1Ys057NaHDR'
b'OaQoF7jnqXkrQeKQoCvmEarq78Dgi13wBqH7E19Ggj0Tq62kmsDDzuIimhthmlq2AFMTOU'
b'toIggor7fL38WwtnpGsLY6xtzz0j6NuNh0YaN50Oz1u5uhHTWQMMcqtUYYHL2p8pmeQWeQ'
b'2epkT2Fzl1wtjsNVMzpgv647O+uYoZqcw8UDsiZR61OFJzNR3VHuRpfxzGG9WFQfddd9YH'
b'JFnEgAMNmXt0Gs/j/C5bzxhllcfH7icOl8zm6GGQUQDe4akfTsExcjMertF565VtDPrP6m'
b'QrCn18xxNSFg2IyP3rO55QrpENR05aPa8A4ZBkKdHUkKEF54qOygAVaECXE/IV2TSgw1cp'
b'qhkYk3s685KA48Y9U466vSJnOPhDxxwqZSwv+R0SgIhOehLHruIc5CflF4yhzDzrBeMpmH'
b'p5eK7pKDXI3a8SZgPqNVBtwmMm5SLZaSuGDKSzB4SWsBPDBeJa77R0mCeRfjat4m09eJPT'
b'IuHhgKvnT1YLj3/vnZNVfe1ivPfWrqrI0Y1XT1bzaxfXwcy8o2tW41nfe/kEffmVi+tgbD'
b'7IYDkleb8x+kTjvsUwZmYQljsfuDKfQdeKgKBtOTjoVh7wV7Is7L0rAZQbchzrztyMM+ar'
b'AG+6GvPJGil9LbHrYWaxMEVzpf6tiN7Q3BcLE/jzrZBMhhlptuOsX65YL8f6fjuxYHdDsG'
b'Vde+ZVRAvPuTW1WK7uEPL0zkwnnLtb46tyx5iOT2I7X7RIvd3mnyF3UFuN1RRi1UoQSK/0'
b'5MhcpfSQI0pPY4n4lHG+BBqrQvBk7VWhCu60vaqjxWsVSLGsy1Eo3aO9clpf9jY38PiYO5'
b'JL67EJDwXxS8zGpoEcjt6gLcuWc4NHNmrW59hALXNo8AuV3UDaOs1CsovFWM3xIYyQvDTR'
b'XaCAGKK9QzpAtqH3tS877+Ij4CwermWxfsbjHgC+Xo+RaBe60ZyE7kcJ6NER5aacI7rd1w'
b'FKb/+gTPLTgHo7ewXdWFFo8xts7xU8axbr1jEyzC+jU4dTJDGMrEukZ3jYcqvJ7dSCPTxR'
b'gbcXimWVpw+DMeNbKFpsNDPeqetwc/VYhuox7MJlnxk6zYF7rJMUw6q/QMfsRZmrdVbttE'
b'3ie3UyT/OIEeKAE5Tc8A35YM65oD7JaAwh3QML6RT+/NXlPFm706tBiOMsl3Qgl/1TTBlq'
b'01XJsPLEBTMJyK1yyZLvFgtYf4ZMzxMeuENF3Os7WtrEL3hSB7Df+p7n1GFuF3jqyGBlun'
b'RIdPVuTtAtHDBUfwkMY9N3wFg6XAFDmkq9Ots4nwoW3yNlcLUFTr/cskOn8UrjPNN/MKdX'
b'Nab2Me8oB8LBnGqm1zsaDYZb550Xpq/vnuNYUHQe1eHXjYV9yLUlx2HWc+LQfrh+oPGpwv'
b'1rGyyV/rzuMQnRTmcB9rFVBsJQG4u6CnAka+tw733m6Ctpl4aBrirO6CzAUR6nDvfhzh19'
b'lbMTMt7W+0HyqwSiDRlaRUeGDEyTPYFIKQ6nN22jwXz4Q60dNQzmePKu0fO7WU+oYAwvrB'
b'SgyPUYivDC3VhLlFEYN1ENRtMRVD9tFjdNDe07bKj4e70aCZ13f7UaiXZ+Q6FoW+t3rJ1M'
b'HXqtgSzTwBo/SsKqOZojovfb63WMmt77b7HlGLJSr220qaJ1CbF22NOM9LEPOqkig0ZqwK'
b'AektSjZsU0cikoFFjhkOfuEWNLwMsIj3sRz4tRhOSs0iokRs/MkQQz0qlrgaKdgsLwzajV'
b'oI5wKe9q+SJz+GjxwsHjyfQ0iRcEWXsIvKCK62lzNfF4NMV23uMlQOgrBo0CwPRxHxnAkd'
b'YtT9NRuTLmg7mB2iQCn9pcynF9A6FxhgHcTUWVpdwV1hg8SdLoE17xfezvI0tDdh0AA40u'
b'iqP8rnuS2S6zQi0QIL5xi0QskX6Can61QDBDevUCQZ2RVgsEKAi9IsAmenNFgMPFEORZQp'
b'5hL7oPQ6FGE4SrIkRJjfYp2of5DiwMMiEEqIR7rYEgIcF0DMSFtRM19ZL6D9XRIRWXh23Q'
b'g6HLEXDHNkpk/+UxuEZnd/Fr2I0hAg+ZqtccapSKXnNoNR3lF7LkosqPArob0CcT1peLOs'
b'FK6Q7KQp1FSyBu0ARPToE09sRzDZiLBkqTUGCP6BXttd18IM1A3Pt78RgzUOU180utkKBw'
b'L2qJBFnydd89hfzFFHevnCM1rzEfwSv/y4SqGdrrQWttNUlM2cwBooNfbZlO8e1VLTrRqp'
b'alg6pFWp/2mCeH6ByHpqNhtgBDnr9krDMAodDTRN/kMmlA2lYGBXOSHPzEE2PNIUw8MciH'
b'c63LpSXiiSc0skM88aSnaFgtDC0ekDPRbYkINroeUdNRCiFa9wr1/w+rTtuH0A+q0kOU6A'
b'TsjLRfWjeEXlp3QFhaJ4Aey+toLEK9TZwn5hYae4SJo8VhPJus4ITGIlcLtSuHj8YAB8fv'
b'EuSFR+MwUgvHJtN5adEATC0wHoXK2uORBC7Q2GllwXP/3F3OAWZUutyQ29EFipqOyo0ezX'
b'qJ1p+Z/Q71GiUKntO/Cc998SucGbe0ml2tDBCOXNeKvnWJV2b4fgJmfeuj6x4JR9ctEh9d'
b'nzksHF23yK2j61YifXTduo3WPCykD6hbRA6oLywpZ8YnnvYH1K17OaBuY9UH1K2D+L6yTD'
b'A5oF4GSCKbW8ztlCAgsxoCkeLVEDjTW2B5IKPBA6ULXcDMPqgXcCkMvadeIWGPFY3+4KsR'
b'BfFEnW1O2nerhtD9qgNCx0oguEdU0WWZiCq6LFPTUWWmxwOGr/UzzcRVD8prWP0NDTlJ34'
b'+wlIdB7aiWydUDg21rwaftBUKK02au0NEZ/ZVh3TqGUt2ZsyRkX/MMfGsZdpkF1tUMpDG8'
b'8XSmduiNwIrAugqsNbzrRxahmGDU57MA6/5ApWbCRJzVlWwzRfPVJY/4dUAWw1mpSCtFHw'
b'ZZL8TkIcL90VcTWL8xj/nZAJknZ69itZ7QQZkoeX3wbtcZU7DSAEdeO2kujK2Ni9Pl3t6p'
b'Vk8tidERKiSB1AJs1NYF8+5VT6kQpOiXkFEpOfCrGzvS619vXYF1ofKHTI2uD0WeRteHaj'
b'qq6RUZZ72DtLCIX8J0pF7zFChsHxHa37PHejKHE3JFR4cRNEMeIlkl9mIPax3lFFrMMRVq'
b'3k0UVmFZAxf8kG/mDh5otPiQee1UkcHsxIDhch2QSh1EqEr5Q2t403pGS9rrGYbQeoYDgp'
b'7RJgN1x1Uy+BMU6DSHsOucLZPhfn082jlT4Qlt7jjz4C3j2QbMIByC1iZcZLrjF1NIEF3D'
b'mqYe0PILeGUFOrviaFNQw3WHOzJ8ix7ZWkIOd6ymGvALlMtUo0qBXM40w9+JuMw1qk1s0R'
b'cN1/emYr6iTSFzCMXr4p3KXqSGlAMmKBGfR4hHGTWvykDqMkDo2oAZ/k2w8Kyun5wn3vqS'
b'B/ftt5uc18ng7YtXyDxdHggjMmlB8vQOMgKNDIxXpI8shXlqPyWHG0srQdvcQpKrS0tH+e'
b'lC9DnZMtjoqJLJPl7EjFF4uLI+hne9wz1Pbm/XI1khp5CdegkQgos9MNTGIb4wk7kcX5hJ'
b'efbeomWCb8zsaNY6s58pH+Yt7bfet08tZOxb5SrIqrLocUAfoq0vG4ufoebqmlUtHe7MYq'
b'FaDHtVnkvK09vEcJbpCHG+AKKVIriwSnKaRO+IG1KpyBXpoCFPAnnrbqc52V4/Nl5RKzpo'
b'bOgbzIMqU2L2Ni9e5tWQfOx5YzbvW1+Q1Ap1ZYGgTxsgVqdTC+14UR+GqSFWrQ33lmZtUq'
b'IVa+My0qsNcutGKJMKrW8bl6JuG3a4Dqp2pFe2jWN36pEym1SL7m3kCjadk2ZGwKvPqSX6'
b'Iy+jZA0Vw2v215aQOt0uCakhg+6vTPvpz91tCsFFQ0BRAhWrcGiWNO2iAXmeoVEdN49GXz'
b'OViI6Pm/369HDZWaQhct5SIKPgpKhv+n7PNHP01WgAj/5h81XtvuUCKoYyNveeOUz3BmMs'
b'WsRFgq0xRRRsWFBboQj0mQboQ4PoQ4X79r0E+w0DqIPybFyRWTdKzT3mwXXPVqh4t3KexE'
b'9+TAoBwn7lLGD3u9f11zeCCwE90hjk9DAcO7v3N9w6lNEo2Oe/xvQ43CQvfLZskrys1/uX'
b'oDzWBuFZrmATlcGxnmPNQfpetcC3nz4Rf+rMzZ9ZigGBlLnyAoP7SzQPMy7VNIy0XsxOQf'
b'dva0wH/CZUxuD0+jaduLPAxkh/9DTNlOzhYRvZQS+YuNFCPMNFxOxOWNHLRKvtTN2xO7gL'
b'ajD+Chkf3V/mbWCZ94XRWAWwbxgvAqD7KeUuUnxVXKL3zhSmFHwVhH0BuQmAvnjZpcbfrZ'
b'PNFD1Oz0rx7IPJtULsWZVKITpJrcKjNOkIJVFzDapU6VDse8ulQnS6DM6Z5qZ/NPO/DMCp'
b'Cyf2Tbmfolt1KUpYkCfl7l+p7GeaamKjiGytiLBF6YDxqXgHX52Kd3h8Kp7gN+UKutmLXp'
b'9FQoPCjBLSC6rQhuzNoaj50Qk4uAuXcUynQoVJDrHuW9ilyVF/rN3b2GUORjAzZhHFhxzm'
b'ib6wlOGOzlUYKceLE01RGzS0fxPO6FJB1v7ozgs6unnB25yRxMcHKOnRPVDMVm2JoHXMPR'
b'TVV3EoRkTGHRUBBNO6b612zxxmhwKqhtxZtFg0aqUO1KfxvcNIBh+LtJfMA2rPqDbYCTUF'
b'kphZrzNINY4x8G/6B75NisYxN4milcDJ2O9gYAJw4r3XGe/OflFL50ht9EZQQ9r39obQnb'
b'oDQq9OwLw5XPLD6NNF4s5FXO2zzoUz2mkVxnjte5GMz1hg9HbQaEXbOPUn0qqa1OEsdhe5'
b'iSI+4mEktTbgc/P5El4qxlzdABeZnKeMYDiteX++N8eASvpiUs9fyHSV4tzho/Q6OF7/r0'
b'qPxnlQWHhkwV1lSbyFPHXAKFucbzMgjkKYKpaEosDRPkDlgjoz+8+hRDAvsvjIOROpGzxD'
b'1m2b9KhAmAOvR93YEAj3odEUG/OljQ9XBgnb2IWh7c73hCc6DGk3tUtHqFZnA5Rmn1lSjU'
b'6oMtoD5o8vymYONSy6ngX1cuAhzcNTD83sT6pI/rIkSqp5HLSFt4h5ZuQTZhszLy/CYXQ6'
b'N0m/iAFfisTpJ6ehvAf60R6OZ+WVuQPch5VLphyasbnkz8wfUgqiHrKbWSpY/vFS6ZfjsL'
b'k8mOXaFYnfeXz1q7lFxTC5+N9t/G7BgtBLtzOWgjQkNeQxLJdmgoQF0txgmIPYY7F5pWg7'
b'aUE2nEyLrPmhpwQpgV3/nWcOUT/U6ipyJrrNBfFEd7eAVmuEqMhqjXCe/EGtO03+kKM0Nb'
b'/3ygCGgDp9l5EcGVmXxK4MjSui46N0DM1f1ea/00lErSPqQVNZFVEzTeW5pjidClRQaTwy'
b'1os8/gfPlX0H/l/9XGlUETfWq4T1PT/Xzo+Hjtc6KI1xlfyhl0xRhqKLtZPkD2eCNMdn1D'
b'HA3cBTlRjd8REUMUUGNcWA0X2AbWVfe43woGKNuP5+O4unMT7yZbkBM6S7Gsu6mAo08moZ'
b'7rCBhWYCjdwaRpyaSqCRW8OQ+mqxOmAj15bj33y1WBOwkWvDifOnFGjk1jLc9f8Wmgg0cm'
b'sY/p1XCxUCjdyCIZ3qInG10Ru5IKN8Wiis+U5rTWWFpvJUU6H2emTcejx+1Qg8I24ERHmR'
b'j7E2xiTCU9IzpRoL74G0gronQJpVhPjnPRQs2zTBb7RwF1x6z0YeZwuE4T8T6n59Mq+wto'
b'K4W2PThSDRQB+8mlGLw2EbQzKQ5XxJ3bP8zbMe8tHUgVQjYNpY+BbkA5op+mBNdQxgLrr1'
b'6ZorjEtBWaWBKGVVwvVGqILH6Nz/ArTavZuA9NsbRSKbPjnxjdvwRKyOsCsZxt3IDK4dYc'
b'oQbkVWIJcJp2asYqtETdIcrfcNJ0l8NwdpbaI2A61N1DQdWRkgK9ZmQxBjo1nCVIu/KXjO'
b'SvSayRj3J7tTQuNOcx8ElYsy0W8spSD9rhamqcdgK4X5bnhLoUVcsVUU2WpHCYPKMZrTzw'
b'zt92GKJpByJqdAfnaYQ/L5J6PQQd9qCKGwgsJUChIUJsTdPfGBHTtPZRE6mpsALOg6IGZL'
b'YFVi0n1UKwB5asmgk08IjA4eM2BdbgvSb52x49UH5fL0btWucvxTt3fm3NwxMlVeKDoqXw'
b'plTrcZiU/b8bBq0Xhcre3IGTNCfz1my8hR27EzZoz8OXYALe0H19qOoYKNfDuOH15rO4oK'
b'NnJtOXGyqoCNXFtOGGJrO5AGcOTesWSQre1QGsCRe8uKM6sM2Mi14/iBtrbjqWAj15YjQ2'
b'1tR1TBRq7JsZ2tXezPeIsdoF6pdJUFaBS7VuVlcXWoyRxeOvIFHW9o3gZSXUNfoQfTCyaY'
b'eB3DoXkSA6cfKT9sOEv7GYyhGw3ou0AKMkbXUJiAzv0Dfbi5LATDfHt3tdiQOny02ODg8b'
b'JCbuHRTawTi46Pi881HBsNzhxL3DogNpJnf0X0yjxx4fFo1cIJN178gU5g8WjlI18oNA7d'
b'xRofZ19acLyOkbt8HZs/urQj5cd+ZIVZMiiurJuh2uyZ2bXs0THJmYOPvXfJgVCvjtSMRX'
b'eEmo46QjTXnlZ0PEvJL23ZXxjE7UVZNv06y1UTZ0C0RjeLOFr0RcQJa57ZMheO223ImjaG'
b'9Lm1WczSAWVkxbYCKQM/RydfMMs6aqPBAqlx5wzYqBZChYaGHIjmaYgoOj+A0ovOC2g6yn'
b'NUI4giJwQgnOj48KOVreWCtNewUhL6Cg1y9bVEqaFH9xIxyOsTopOA+u16BekteAXf2kKc'
b'3mD7rcRbPL2lCL7edoX4Z3/KdoZoQ9bPPKH7N/iOzh8gW6PzB5qO8h+hIRij+yjNLbNonL'
b'xVTrTnq90l+2Y53InIrw93NskoTycB0TfuBfRWjubJdzP0BkvnZ55wqbLCj1bY6+QkCnvj'
b'vrXOWBYAN0GnMqSrcvS7iZWzZk5svJbUMOTNaC2pWQDU+nlt6KCfk9Z3dDBqfQmHpiOrHs'
b'YGfRn/b4cLYnzbdq9rA+3DyX4Kuu+ejZaTuu+wnBIjQfXzeNAOiGBK5Btsnlna22RMHb/f'
b'8/+dXCmC6h/wS3hmLbfw3gfnaE9ODCmBW7Lv9enM0mHeS2Fp7cRB3oUVRc592hRcuk57qT'
b'3oPVUO0I485t1YUWRfxIUh9Cw56VkPSD/rKVP3HVVFBK+mQitQ29c1LVNm9lNf3OmgG2Zz'
b'y8ay/PO6qAhhSpVZQu6Yg5Z1iuZYGcWMpEoN7YcK6DpCRs7grUP13u30SIUm0D0Mdt8sd9'
b'+jx9nmib+bccL9tFPXqaetckOPmmBmwKs2aN2OGyHK3j9iUdrPNNfEoyKyB0WEebYDxgtE'
b'Dr5aH3K43j3PkhuPVtBdtBu8JKD6A5RjdK2WpqP+oAVj3z8MO7v41AQyrD4pMFosUrhsmU'
b'4N9nXoURs5TjgBZosbeDS2oMp2+m7NLEtGpjEspK/mgnU2MH6GTWUHqHF6aZFggFdq4NYZ'
b'lYl14Ed1F4B6QLO1iB7jlx4KhnYOik3tKg8G+zoH3bKwc6JqQw/nOsp/h2lzOgeJQd3c0W'
b'JS1wrgjeqcFzGjc5HrHTjnJD7EMgmgnGKZKkyOsdQOdIZ4COzxLHflQ3E7baNVs4qAGoVL'
b'0vrCtpoAbwSSa/NSh+jnkVaLMoLDnXqrBUvScPSzSPAw0bC+hK9wTyJZtr60D74yDUfRrB'
b'K538I64ikMo6TlltzZFUlef2Fo9kCXvXJvlQmTBVodcEDQBwyww1R+px4RMbHoUQRj2/Yh'
b'zkx0vduo25xaYNRvlha96jgri497ThaRvtKOgvDYoD0yaL+dmB4x6xLNxH5CVE1pIss00S'
b'kidI8OGPe6Dr7qdR0ed7EEo6xiH7rlzceSKlbd3pxvmJmvoCJpOihIGjVfwxlwtriGxU/M'
b'FC/LKzT4cLwh1INFaqCgl1lBlAhzDYSgHCzOGkUHV0StvlCj1vZP5jFRqtT8pCnKwsGmTi'
b'l6dzmsz91ooYU8PZKhhukJeaPpaCRDTvW7i3o7ZmmB6MCzAfe9tc+hijHKKcY+nK6WdKYW'
b'Hq3oWHRkPdI6MF7lKZNblh/zJDb6KAwdHyilxt6zz48WZmx4o/tLl8ktcxEmkqc82Ef0f4'
b'YhyZBqwDTuwnBZBPKWvfqKbD9UGq96WHRAGBQNEA+JpYXCgGiAW8OhEUUPhsZlNBQaRA+E'
b'BpBhcGYoGQSXjvRDoHEsA6CJTg9/hh0/MbwS6HLkfsDbBuPwHvU7NnefeWcyQuaCyPhYGc'
b'iNjojL2XBnK/sZ7TQRs4c3K/epFekZ6oq+bhz1K1p4QeTcDT6pVrIwWDwec0d19O4eyi+6'
b'E5KudKvUdNQqIeWw6zcXI6uxtV6/OQW/9ixjzh7zkCdcdBKTZGQk2l+4GIt+T35WNmlIhX'
b'UhJNudC80m9lPXPAduzE6w+4yeWVOYPLM2TU6y1IQWbnRSPVlpHPbwwAswpp7a89zs0lF+'
b'08vcyw394mHL1w4x2M9nzkV4HslzfEjPTzQSXHnKhNsK9bB+6eGJUXtwd6BxVOqpgf6XmS'
b'P3JjTvFDWGzMKTJvCFp5zs3E70oYXzCddJKZ2bcIHRYLYDzWqjd1RpR3ZJ1rqiB++odo68'
b'+bHHvZymbF5RQ8zcw5Ueb7Q4HYN1GMolWtKpSHu1yhBarTIAn6TQPTqHbaLxkjPXCYjGj1'
b'XUE4uO1+0zC8c9e+mCGNkP5haNR4bSgqO+nU1IrwMiGnsqgs+RMyccFd1BhlI0ZziuG2Tp'
b'ODfaI0RVFmH2Wx38recOCwdz2UmHQ7YcxS4PW6rVNEwjpbsTZHH0pqymo+5kmcSvhxYUht'
b'q9tURLkbgLLyPh0B4ZrHlKC90IqsRGHQg2ZUsE8zZcXtfRvU6LhLbNUAr04dw5yYdneyQj'
b'c5Q1VeB7UHJqNyNH2/JaOpjyklbbvhXJ0fvcGbGr17nz5BytCa5IjzTzBUPvmaYoRcvkHC'
b'0frhQdnUmegHF+7bqdvuf8vOZBZxP0V6qXc34Y5ZRab6C2IzJoxgYM+ilIe1kn5s1nbZUP'
b'hiyDFfjG6Mu3DdBXnMPqV4mMeNDPW6IqGiBe30eVNOjYQp7F+3D1OGTDPLLw1Wl7eDEXjy'
b'bnsFiWWyK+q6VKgUZWCZRVnX+CLnCOVsYaQ8sCGmTQBw6mqAjdrccG5nSoLimfkxw941AS'
b'u3Hp6zzzjPHFAZMFOVcPP1QGDQfcTcC3bjjAAOI5V0E3ZO35cO9ZvSs8U+hI/KlhxbV7Vl'
b'vwRtRT4VxF3ZJ1fRtChaKJ7sUpFR01CjrcdS9bngvNeGZNSK9TmDh2PSft3WbQd7BNPOOP'
b'jksHgcGkK4XTkLeUY8MQRXdpKFEtKUpY2aFTqpZ8KO1sXx1lhp3DhXOKDBfOGTBcOGfIk6'
b'6GDZpi97UPM+pZY4Fo6kUwOuJQkPa9oiF0t+iA0C8aIPQ7+cTQI/uXBUEuNT1jpBndwViP'
b'eNFFjJVm+tX+KLSrKxlRH3QvkzWGHlXTuQGv2ox1O66+jA99Qfdnfzqb+zdyCzzyMGLGd+'
b'VA2ieCavtpTnqk9ntkxE/U7KxfzWZnwhlNaIUxnr42yXiX3uSNgUYzU+P0GM+WFoLJPGgS'
b'IKmtTB60SqOvhLs2UybEHQ9Z8vPFnCYRdkaMVmOTVZtYb+r8SOUgASYWGMKBktoi6ogJS9'
b'Ye2tF302eCnsx7cpzrhens4gY3TDENGyXDeXhuP4NXB6i5+MwiIQczDdyaj7vw/YzcBaAW'
b'r50DPUufeSjM0x0Uz9RzD4a5uoNudUhOVD1fd66jGbvDbh0SLy1LT+eda+nnnJMwpZ8L4C'
b'f1zotb7TNHUdoY4t2aJ7NB7RjSU7o06MPkLjg/Tyeprr9E1Y3u5kKdje7m0nQ0dhgGmtFV'
b'I514xqiNenzcRLNkPDmoHDJqoHQoz7yFR7Wcoj+xkLNdyR01RORmuNzvnJPSeeARERajXV'
b'azUDSDmFrQz+Yciozv9506PEShedIxDBulQ+LBxKAv0YtmlERd/eBOlFDm6FrxCsqtNmAp'
b'QUerJJBUvwfNNhFdVYX+IrqqStNR2TIgxIPs//NMc9qnrbUca4uIIXdGs0FaXLktPRac1R'
b'7a9xsHVQZ67M29Ms3SUGbZjxNVEnw8GB2o8WrutbDShd01hkAzRn+/8ATZwmlgj45m22GC'
b'fUSf0Jkb5GiePf0uV7YCl991ok8Uz266sqZMOR+I/i5bImq/70bHhC4CqrWMGwjZHWv3o0'
b'uTnGWRB6mn/ZA1803ZqXnSW+zOFeRNdhGC3Efo18SR5cd+/bRBsHziwRC7R16aPrXEkTtA'
b'zdwSPMRPa1jagPLZWr4013NO5D7DRCoCwlTKwWEyRSCaNBjAGHZSceNnmmlCc7J7RYRVdA'
b'eMN1gcfLXB4vB4g4XgNrrIDrmnVzPQcvUEe7Yi7W/BMIS+lccB4coOAvoE9czQ8RyQ88vr'
b'KU3DJn41u2jYEcQa7MQAXoW1lNZhPRKUWCLeOKtG5NHNYKgP0c1gmo46FlSPy/g2D47Sl/'
b'F1HosrMDoZjSx67XZflZ7ROEQGWu8kaGm5Q2SwNH4O57ewNZw7RDSGIp9OHSYaYOUBCZkB'
b'8WauPONH0D8MqbSjmnSQOQ3kLc3IhOr1IuN1dLNO4bDvIboPmZCjdajaAkGDMkCsP2UWCt'
b'qTAW7pTiYpWnMyLiO9ySC3tCYjtNaZjEspSMMO+tLMkV5bMo6lSI0c8m5OY7JQK0PGtVeF'
b'HNEfN0bRnCa8RhnxXeR2tXlyMes5GaK9KLM/UuqylxqkuxqtXCYXubwMIYaFFUeEy8saDc'
b'hKS5VEz4HmyWWzDt1HkYIOt41VlpSzIZDd2yFCRH3b2CKQ3jMmxIJJ9HnAJBlzhQXRVmmA'
b'nQDpUkUjdxItS4DqpjAIKTeUQUptJmnI8C4xSH3tD8LR14lBd7i4C8qaif30V860M0uraC'
b'muvqCsbSwdhbi0mFxQtgIdX1DGHNeQzhDk3ZUdMmTUtxSVye3lYXjVt1Ogz7+EO8yQqZKZ'
b'6Ogu148YrzyoluQq43J08xOkj1RGlAVX4PytQcVK0eYS7QlTIJD2m2u3uqvJFe4vJ6Jb9x'
b'TxnJ/s7cyy9QQlJxdaMRt8u2eRvsgLPCTQiqMtbzQonsg2158tCk/ox4ebMeh1SBO44fgL'
b'HzAPc4jcn4bK8DI2xPeYO0kBEaL8ZQKsdT0v37+Mn8qGwnc1/E2L5Gr0m4+xaPBD3UAPtz'
b'ZW8GrldBXgq1czG5S7f5KY/qP7rCoPSCeA6HVvh6yRboXfusVaOjRZ0le1LgN4y+45wr3F'
b'cwRqW2cwbgWSJtdhaEwHkSZf2cWXyVfZSyvwrbfSLB0MlEjrW4or0NwsWJIRtgdyRZbFCA'
b'hLkgYMS5KWNKe4oAE3QgWt2GDaz2pC5G0IL7uhZ/sahhkEqXo9qEHRS88YW78q3XI+JTlS'
b'LRtiV5rlguhYsVwC1JkzA23ejeDuiu8TzAg6qRYCcBKrngabLCOOPo8yizjhjaI4LAfWAK'
b'Pbb9vkq5/LIE16WWMFt2iC+uEkNHcL+TrkaV1/iJ3WR31XPObpDvNNRADdTgBGHS+qoJ6r'
b'VxDImJjefGe8HTN1UjxTG602yf9isEoPOoB58lU6XVQlP/hVSGxQ+ZHjeiyeoeLogW01TV'
b'5ZyFXy6rsVJPl1re4snYHUhzdWoPXhDU1H8i7IkGBqUOM+tG49qAMkeFZ2uAWF+2ou1uME'
b'ncF+fbs9hCE169ewU8g4R89ImtBfw0uUYTV9GjNib3WZvKpnhpbJa2i5pSXETB3d8Ksaz2'
b'uSaosN85BX1dKhO73q3axZChq+OSbwFuo0RSqixkoHIV+Rnk7dmwrJvKZUwyFNFvTFkAaQ'
b'Rwox0CrAzWWAL2cOh07VHeOFmEn7HZ4qB2i/1278Cstk9T2mDmFqHaHb2huT/GJRRYi7NJ'
b'zn4LjlZSqRclw7x8PrwV+kY5yEk3g8kn7lRrOXls2kfS+IRX7tRrNTz+b94ryja7SmVX6H'
b'L4tRLs2G/m46Zjccab4LxPjzb+PxRl2H9jTYCAZcFhVnLgmnMw0Yy4mTWG0/lr48/7fFu/'
b'r7TiStLhnQF7+X0GLsQjNRFHpBfDYBrVuNoaWZQOaoW0ce6SXXWQZa+9Z0pNQhQwbzMMmM'
b'H5HdC1noSf1GUIY4pL9GeEbfTLmF/KrPysFV6L1RB98OZqK0Sjj3xHDzpxqB82Xypza3zp'
b'JgT4lZ1p+6F4LTqBdqkj+jEx3QCf7kBUpNm0SWjui4xawRmfynkrXNEz4EBD30bb3ehA57'
b'2ib6tnRouG8yM18mcnF6Rlz1ZFkSXaNuvOmlLNJ68JiC1uOGpqOByDAkmhTUfs3h1e+6Ut'
b'yroSn3oI7iCozqwgJcrdqXcB7Ko7ZEGCaq5E3P9JG8qIAsLdPgInlTCuB0TtLcCB+GsGUW'
b'wFg3ZF6Od4pXxvWtkbCMGaORcB5zxzvNqFgRf7TlDIXk7Xp7GlPwt6vdaegmb7eNKzD+vn'
b'3HuALV9e2WccXMBGa3LIezXTcJGYc6oSoi029MU5nncZsmokZbQ16dDq8ZwHG9RRN4Q9sM'
b'JhbzCI8fxjI8fXHZlBl5vLmCgwYHKDYETAUbH7VnVXasGGcFOPdhijKDDF55YIm4bYpmaj'
b'/9agumUm+91oGRC1rwgvxgdIhY+sMb+mmMFWzD8eYYhYi6G6RtMA9mm48wT1NkmJYZMEzL'
b'DBlNsTKH6PsyVk0KMaID4ag0QxC5Zji62deKjnqWkgypDSiwqzuvoe29XV163V6BUT+C/s'
b'g8VmLPJ6AgBt1PGmFVh2ZieJNttIxJfgtv72KWJkvgLMmX4alDIe9ZAryXaR5D+oJRlCtt'
b'4uZIpR+skDN6sIIoftrBShkGLiQhOvGNIC4qg9EJRAfAS0VHGVyQIVVpAup03z/pPrZxWD'
b'+c+8c+ejQDQxp4u/4MPUTDVYBv+ZqRPS7GwoNa7CswKkbGrroVdowX3XuwJ9Xj5HJF2i8Y'
b'r5JvHFvnyTd9WA36xjdZRCbPO2/wrS8cIK2MOmuSI6NOBnVt1FkZNBh1Gldjo04G16szXJ'
b'mhR0e4JgC1jSdD+qN7xIRbHVhFCRs0visQvfW39fEPtSnPGN/M2adlaT9D1xABoXNwcOge'
b'AGhtCSn1S+VVi28ZqWeWcCM1an0KwBp+8tO+sV4tzJcYVjraj9ezPPkWLeAgtpuWk2hS37'
b'pbJ6NRAaITtgg/OmFL+mh2rybmK2z/WFrtX5UG8FtSltJ7Sh4Jm0oWiXeVbLB6s8gi0W6R'
b'hfSukEXUzo8F9HkXi/jtHUuZZvT7wLfOqAusAngYDg7PJpNFwK0MwFD3ndEakhGdR0ShbD'
b'vdnOYEzKK/vko+I6oLj+HcLr3KcG4U3zL5Fh0rQwWOjpWRPgzqPnBUQW0lwoYRDYwQNToR'
b'A/fRiRjQ0s/D79gsABOib2GDDQmK7OEReGQPP0/+7a59v0z+H+SUGTTsMAEA'
)).decode().splitlines()
def get_tessdata() -> str:
"""Detect Tesseract-OCR and return its language support folder.
This function can be used to enable OCR via Tesseract even if the
environment variable TESSDATA_PREFIX has not been set.
If the value of TESSDATA_PREFIX is None, the function tries to locate
Tesseract-OCR and fills the required variable.
Returns:
Folder name of tessdata if Tesseract-OCR is available, otherwise False.
"""
TESSDATA_PREFIX = os.getenv("TESSDATA_PREFIX")
if TESSDATA_PREFIX != None:
return TESSDATA_PREFIX
if sys.platform == "win32":
tessdata = "C:\\Program Files\\Tesseract-OCR\\tessdata"
else:
tessdata = "/usr/share/tesseract-ocr/4.00/tessdata"
if os.path.exists(tessdata):
return tessdata
"""
Try to locate the tesseract-ocr installation.
"""
# Windows systems:
if sys.platform == "win32":
try:
response = os.popen("where tesseract").read().strip()
except:
response = ""
if not response:
print("Tesseract-OCR is not installed")
return False
dirname = os.path.dirname(response) # path of tesseract.exe
tessdata = os.path.join(dirname, "tessdata") # language support
if os.path.exists(tessdata): # all ok?
return tessdata
else: # should not happen!
print("unexpected: Tesseract-OCR has no 'tessdata' folder", file=sys.stderr)
return False
# Unix-like systems:
try:
response = os.popen("whereis tesseract-ocr").read().strip().split()
except:
response = ""
if len(response) != 2: # if not 2 tokens: no tesseract-ocr
print("Tesseract-OCR is not installed")
return False
# determine tessdata via iteration over subfolders
tessdata = None
for sub_response in response.iterdir():
for sub_sub in sub_response.iterdir():
if str(sub_sub).endswith("tessdata"):
tessdata = sub_sub
break
if tessdata != None:
return tessdata
else:
print(
"unexpected: tesseract-ocr has no 'tessdata' folder",
file=sys.stderr,
)
return False
return False
class Document(object):
thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag")
__repr__ = _swig_repr
__swig_destroy__ = _fitz_old.delete_Document
def __init__(self, filename=None, stream=None, filetype=None, rect=None, width=0, height=0, fontsize=11):
"""Creates a document. Use 'open' as a synonym.
Notes:
Basic usages:
open() - new PDF document
open(filename) - string, pathlib.Path, or file object.
open(filename, fileype=type) - overwrite filename extension.
open(type, buffer) - type: extension, buffer: bytes object.
open(stream=buffer, filetype=type) - keyword version of previous.
Parameters rect, width, height, fontsize: layout reflowable
document on open (e.g. EPUB). Ignored if n/a.
"""
self.is_closed = False
self.is_encrypted = False
self.isEncrypted = False
self.metadata = None
self.FontInfos = []
self.Graftmaps = {}
self.ShownPages = {}
self.InsertedImages = {}
self._page_refs = weakref.WeakValueDictionary()
if not filename or type(filename) is str:
pass
elif hasattr(filename, "absolute"):
filename = str(filename)
elif hasattr(filename, "name"):
filename = filename.name
else:
msg = "bad filename"
raise TypeError(msg)
if stream != None:
if type(stream) is bytes:
self.stream = stream
elif type(stream) is bytearray:
self.stream = bytes(stream)
elif type(stream) is io.BytesIO:
self.stream = stream.getvalue()
else:
msg = "bad type: 'stream'"
raise TypeError(msg)
stream = self.stream
if not (filename or filetype):
filename = "pdf"
else:
self.stream = None
if filename and self.stream == None:
self.name = filename
from_file = True
else:
from_file = False
self.name = ""
if from_file:
if not os.path.exists(filename):
msg = f"no such file: '{filename}'"
raise FileNotFoundError(msg)
elif not os.path.isfile(filename):
msg = f"'{filename}' is no file"
raise FileDataError(msg)
if from_file and os.path.getsize(filename) == 0 or type(self.stream) is bytes and len(self.stream) == 0:
msg = "cannot open empty document"
raise EmptyFileError(msg)
_fitz_old.Document_swiginit(self, _fitz_old.new_Document(filename, stream, filetype, rect, width, height, fontsize))
if self.thisown:
self._graft_id = TOOLS.gen_id()
if self.needs_pass is True:
self.is_encrypted = True
self.isEncrypted = True
else: # we won't init until doc is decrypted
self.init_doc()
# the following hack detects invalid/empty SVG files, which else may lead
# to interpreter crashes
if filename and filename.lower().endswith("svg") or filetype and "svg" in filetype.lower():
try:
_ = self.convert_to_pdf() # this seems to always work
except:
raise FileDataError("cannot open broken document") from None
def load_page(self, page_id):
"""Load a page.
'page_id' is either a 0-based page number or a tuple (chapter, pno),
with chapter number and page number within that chapter.
"""
if self.is_closed or self.is_encrypted:
raise ValueError("document closed or encrypted")
if page_id is None:
page_id = 0
if page_id not in self:
raise ValueError("page not in document")
if type(page_id) is int and page_id < 0:
np = self.page_count
while page_id < 0:
page_id += np
val = _fitz_old.Document_load_page(self, page_id)
val.thisown = True
val.parent = weakref.proxy(self)
self._page_refs[id(val)] = val
val._annot_refs = weakref.WeakValueDictionary()
val.number = page_id
return val
def _remove_links_to(self, numbers):
return _fitz_old.Document__remove_links_to(self, numbers)
def _loadOutline(self):
"""Load first outline."""
if self.is_closed:
raise ValueError("document closed")
return _fitz_old.Document__loadOutline(self)
def _dropOutline(self, ol):
return _fitz_old.Document__dropOutline(self, ol)
def _insert_font(self, fontfile=None, fontbuffer=None):
"""Utility: insert font from file or binary."""
if self.is_closed:
raise ValueError("document closed")
return _fitz_old.Document__insert_font(self, fontfile, fontbuffer)
def get_outline_xrefs(self):
"""Get list of outline xref numbers."""
if self.is_closed:
raise ValueError("document closed")
return _fitz_old.Document_get_outline_xrefs(self)
def xref_get_keys(self, xref):
"""Get the keys of PDF dict object at 'xref'. Use -1 for the PDF trailer."""
if self.is_closed:
raise ValueError("document closed")
return _fitz_old.Document_xref_get_keys(self, xref)
def xref_get_key(self, xref, key):
"""Get PDF dict key value of object at 'xref'."""
if self.is_closed:
raise ValueError("document closed")
return _fitz_old.Document_xref_get_key(self, xref, key)
def xref_set_key(self, xref, key, value):
"""Set the value of a PDF dictionary key."""
if self.is_closed or self.is_encrypted:
raise ValueError("document closed or encrypted")
if not key or not isinstance(key, str) or INVALID_NAME_CHARS.intersection(key) not in (set(), {"/"}):
raise ValueError("bad 'key'")
if not isinstance(value, str) or not value or value[0] == "/" and INVALID_NAME_CHARS.intersection(value[1:]) != set():
raise ValueError("bad 'value'")
return _fitz_old.Document_xref_set_key(self, xref, key, value)
def _extend_toc_items(self, items):
"""Add color info to all items of an extended TOC list."""
if self.is_closed:
raise ValueError("document closed")
return _fitz_old.Document__extend_toc_items(self, items)
def _embfile_names(self, namelist):
"""Get list of embedded file names."""
if self.is_closed:
raise ValueError("document closed")
return _fitz_old.Document__embfile_names(self, namelist)
def _embfile_del(self, idx):
return _fitz_old.Document__embfile_del(self, idx)
def _embfile_info(self, idx, infodict):
return _fitz_old.Document__embfile_info(self, idx, infodict)
def _embfile_upd(self, idx, buffer=None, filename=None, ufilename=None, desc=None):
return _fitz_old.Document__embfile_upd(self, idx, buffer, filename, ufilename, desc)
def _embeddedFileGet(self, idx):
return _fitz_old.Document__embeddedFileGet(self, idx)
def _embfile_add(self, name, buffer, filename=None, ufilename=None, desc=None):
return _fitz_old.Document__embfile_add(self, name, buffer, filename, ufilename, desc)
def embfile_names(self) -> list:
"""Get list of names of EmbeddedFiles."""
filenames = []
self._embfile_names(filenames)
return filenames
def _embeddedFileIndex(self, item: typing.Union[int, str]) -> int:
filenames = self.embfile_names()
msg = "'%s' not in EmbeddedFiles array." % str(item)
if item in filenames:
idx = filenames.index(item)
elif item in range(len(filenames)):
idx = item
else:
raise ValueError(msg)
return idx
def embfile_count(self) -> int:
"""Get number of EmbeddedFiles."""
return len(self.embfile_names())
def embfile_del(self, item: typing.Union[int, str]):
"""Delete an entry from EmbeddedFiles.
Notes:
The argument must be name or index of an EmbeddedFiles item.
Physical deletion of data will happen on save to a new
file with appropriate garbage option.
Args:
item: name or number of item.
Returns:
None
"""
idx = self._embeddedFileIndex(item)
return self._embfile_del(idx)
def embfile_info(self, item: typing.Union[int, str]) -> dict:
"""Get information of an item in the EmbeddedFiles array.
Args:
item: number or name of item.
Returns:
Information dictionary.
"""
idx = self._embeddedFileIndex(item)
infodict = {"name": self.embfile_names()[idx]}
xref = self._embfile_info(idx, infodict)
t, date = self.xref_get_key(xref, "Params/CreationDate")
if t != "null":
infodict["creationDate"] = date
t, date = self.xref_get_key(xref, "Params/ModDate")
if t != "null":
infodict["modDate"] = date
t, md5 = self.xref_get_key(xref, "Params/CheckSum")
if t != "null":
infodict["checksum"] = binascii.hexlify(md5.encode()).decode()
return infodict
def embfile_get(self, item: typing.Union[int, str]) -> bytes:
"""Get the content of an item in the EmbeddedFiles array.
Args:
item: number or name of item.
Returns:
(bytes) The file content.
"""
idx = self._embeddedFileIndex(item)
return self._embeddedFileGet(idx)
def embfile_upd(self, item: typing.Union[int, str],
buffer: OptBytes =None,
filename: OptStr =None,
ufilename: OptStr =None,
desc: OptStr =None,) -> None:
"""Change an item of the EmbeddedFiles array.
Notes:
Only provided parameters are changed. If all are omitted,
the method is a no-op.
Args:
item: number or name of item.
buffer: (binary data) the new file content.
filename: (str) the new file name.
ufilename: (unicode) the new filen ame.
desc: (str) the new description.
"""
idx = self._embeddedFileIndex(item)
xref = self._embfile_upd(idx, buffer=buffer,
filename=filename,
ufilename=ufilename,
desc=desc)
date = get_pdf_now()
self.xref_set_key(xref, "Params/ModDate", get_pdf_str(date))
return xref
def embfile_add(self, name: str, buffer: typing.ByteString,
filename: OptStr =None,
ufilename: OptStr =None,
desc: OptStr =None,) -> None:
"""Add an item to the EmbeddedFiles array.
Args:
name: name of the new item, must not already exist.
buffer: (binary data) the file content.
filename: (str) the file name, default: the name
ufilename: (unicode) the file name, default: filename
desc: (str) the description.
"""
filenames = self.embfile_names()
msg = "Name '%s' already exists." % str(name)
if name in filenames:
raise ValueError(msg)
if filename is None:
filename = name
if ufilename is None:
ufilename = unicode(filename, "utf8") if str is bytes else filename
if desc is None:
desc = name
xref = self._embfile_add(name, buffer=buffer,
filename=filename,
ufilename=ufilename,
desc=desc)
date = get_pdf_now()
self.xref_set_key(xref, "Type", "/EmbeddedFile")
self.xref_set_key(xref, "Params/CreationDate", get_pdf_str(date))
self.xref_set_key(xref, "Params/ModDate", get_pdf_str(date))
return xref
def convert_to_pdf(self, from_page=0, to_page=-1, rotate=0):
"""Convert document to a PDF, selecting page range and optional rotation. Output bytes object."""
if self.is_closed or self.is_encrypted:
raise ValueError("document closed or encrypted")
return _fitz_old.Document_convert_to_pdf(self, from_page, to_page, rotate)
@property
def page_count(self):
"""Number of pages."""
if self.is_closed:
raise ValueError("document closed")
return _fitz_old.Document_page_count(self)
@property
def chapter_count(self):
"""Number of chapters."""
if self.is_closed:
raise ValueError("document closed")
return _fitz_old.Document_chapter_count(self)
@property
def last_location(self):
"""Id (chapter, page) of last page."""
if self.is_closed:
raise ValueError("document closed")
return _fitz_old.Document_last_location(self)
def chapter_page_count(self, chapter):
"""Page count of chapter."""
if self.is_closed:
raise ValueError("document closed")
return _fitz_old.Document_chapter_page_count(self, chapter)
def prev_location(self, page_id):
"""Get (chapter, page) of previous page."""
if self.is_closed or self.is_encrypted:
raise ValueError("document closed or encrypted")
if type(page_id) is int:
page_id = (0, page_id)
if page_id not in self:
raise ValueError("page id not in document")
if page_id == (0, 0):
return ()
return _fitz_old.Document_prev_location(self, page_id)
def next_location(self, page_id):
"""Get (chapter, page) of next page."""
if self.is_closed or self.is_encrypted:
raise ValueError("document closed or encrypted")
if type(page_id) is int:
page_id = (0, page_id)
if page_id not in self:
raise ValueError("page id not in document")
if tuple(page_id) == self.last_location:
return ()
return _fitz_old.Document_next_location(self, page_id)
def location_from_page_number(self, pno):
"""Convert pno to (chapter, page)."""
if self.is_closed:
raise ValueError("document closed")
return _fitz_old.Document_location_from_page_number(self, pno)
def page_number_from_location(self, page_id):
"""Convert (chapter, pno) to page number."""
if type(page_id) is int:
np = self.page_count
while page_id < 0:
page_id += np
page_id = (0, page_id)
if page_id not in self:
raise ValueError("page id not in document")
return _fitz_old.Document_page_number_from_location(self, page_id)
def _getMetadata(self, key):
"""Get metadata."""
if self.is_closed:
raise ValueError("document closed")
return _fitz_old.Document__getMetadata(self, key)
@property
def needs_pass(self):
"""Indicate password required."""
if self.is_closed:
raise ValueError("document closed")
return _fitz_old.Document_needs_pass(self)
@property
def language(self):
"""Document language."""
if self.is_closed:
raise ValueError("document closed")
return _fitz_old.Document_language(self)
def set_language(self, language=None):
return _fitz_old.Document_set_language(self, language)
def resolve_link(self, uri=None, chapters=0):
"""Calculate internal link destination.
Args:
uri: (str) some Link.uri
chapters: (bool) whether to use (chapter, page) format
Returns:
(page_id, x, y) where x, y are point coordinates on the page.
page_id is either page number (if chapters=0), or (chapter, pno).
"""
return _fitz_old.Document_resolve_link(self, uri, chapters)
def layout(self, rect=None, width=0, height=0, fontsize=11):
"""Re-layout a reflowable document."""
if self.is_closed or self.is_encrypted:
raise ValueError("document closed or encrypted")
val = _fitz_old.Document_layout(self, rect, width, height, fontsize)
self._reset_page_refs()
self.init_doc()
return val
def make_bookmark(self, loc):
"""Make a page pointer before layouting document."""
if self.is_closed or self.is_encrypted:
raise ValueError("document closed or encrypted")
return _fitz_old.Document_make_bookmark(self, loc)
def find_bookmark(self, bm):
"""Find new location after layouting a document."""
if self.is_closed or self.is_encrypted:
raise ValueError("document closed or encrypted")
return _fitz_old.Document_find_bookmark(self, bm)
@property
def is_reflowable(self):
"""Check if document is layoutable."""
if self.is_closed:
raise ValueError("document closed")
return _fitz_old.Document_is_reflowable(self)
def _deleteObject(self, xref):
"""Delete object."""
if self.is_closed:
raise ValueError("document closed")
return _fitz_old.Document__deleteObject(self, xref)
def pdf_catalog(self):
"""Get xref of PDF catalog."""
if self.is_closed:
raise ValueError("document closed")
return _fitz_old.Document_pdf_catalog(self)
def _getPDFfileid(self):
"""Get PDF file id."""
if self.is_closed:
raise ValueError("document closed")
return _fitz_old.Document__getPDFfileid(self)
@property
def version_count(self):
"""Count versions of PDF document."""
if self.is_closed:
raise ValueError("document closed")
return _fitz_old.Document_version_count(self)
@property
def is_pdf(self):
"""Check for PDF."""
if self.is_closed:
raise ValueError("document closed")
return _fitz_old.Document_is_pdf(self)
@property
def is_dirty(self):
"""True if PDF has unsaved changes."""
if self.is_closed:
raise ValueError("document closed")
return _fitz_old.Document_is_dirty(self)
def can_save_incrementally(self):
"""Check whether incremental saves are possible."""
if self.is_closed:
raise ValueError("document closed")
return _fitz_old.Document_can_save_incrementally(self)
@property
def is_fast_webaccess(self):
"""Check whether we have a linearized PDF."""
if self.is_closed:
raise ValueError("document closed")
return _fitz_old.Document_is_fast_webaccess(self)
@property
def is_repaired(self):
"""Check whether PDF was repaired."""
if self.is_closed:
raise ValueError("document closed")
return _fitz_old.Document_is_repaired(self)
def save_snapshot(self, filename):
"""Save a file snapshot suitable for journalling."""
if self.is_closed:
raise ValueError("doc is closed")
if type(filename) == str:
pass
elif hasattr(filename, "open"): # assume: pathlib.Path
filename = str(filename)
elif hasattr(filename, "name"): # assume: file object
filename = filename.name
else:
raise ValueError("filename must be str, Path or file object")
if filename == self.name:
raise ValueError("cannot snapshot to original")
return _fitz_old.Document_save_snapshot(self, filename)
def authenticate(self, password):
"""Decrypt document."""
if self.is_closed:
raise ValueError("document closed")
val = _fitz_old.Document_authenticate(self, password)
if val: # the doc is decrypted successfully and we init the outline
self.is_encrypted = False
self.isEncrypted = False
self.init_doc()
self.thisown = True
return val
def save(self, filename, garbage=0, clean=0, deflate=0, deflate_images=0, deflate_fonts=0, incremental=0, ascii=0, expand=0, linear=0, no_new_id=0, appearance=0, pretty=0, encryption=1, permissions=4095, owner_pw=None, user_pw=None):
"""Save PDF to file, pathlib.Path or file pointer."""
if self.is_closed or self.is_encrypted:
raise ValueError("document closed or encrypted")
if type(filename) == str:
pass
elif hasattr(filename, "open"): # assume: pathlib.Path
filename = str(filename)
elif hasattr(filename, "name"): # assume: file object
filename = filename.name
elif not hasattr(filename, "seek"): # assume file object
raise ValueError("filename must be str, Path or file object")
if filename == self.name and not incremental:
raise ValueError("save to original must be incremental")
if self.page_count < 1:
raise ValueError("cannot save with zero pages")
if incremental:
if self.name != filename or self.stream:
raise ValueError("incremental needs original file")
if user_pw and len(user_pw) > 40 or owner_pw and len(owner_pw) > 40:
raise ValueError("password length must not exceed 40")
return _fitz_old.Document_save(self, filename, garbage, clean, deflate, deflate_images, deflate_fonts, incremental, ascii, expand, linear, no_new_id, appearance, pretty, encryption, permissions, owner_pw, user_pw)
def write(self, garbage=False, clean=False,
deflate=False, deflate_images=False, deflate_fonts=False,
incremental=False, ascii=False, expand=False, linear=False,
no_new_id=False, appearance=False, pretty=False, encryption=1, permissions=4095,
owner_pw=None, user_pw=None):
from io import BytesIO
bio = BytesIO()
self.save(bio, garbage=garbage, clean=clean,
no_new_id=no_new_id, appearance=appearance,
deflate=deflate, deflate_images=deflate_images, deflate_fonts=deflate_fonts,
incremental=incremental, ascii=ascii, expand=expand, linear=linear,
pretty=pretty, encryption=encryption, permissions=permissions,
owner_pw=owner_pw, user_pw=user_pw)
return bio.getvalue()
def insert_pdf(self, docsrc, from_page=-1, to_page=-1, start_at=-1, rotate=-1, links=1, annots=1, show_progress=0, final=1, _gmap=None):
"""Insert a page range from another PDF.
Args:
docsrc: PDF to copy from. Must be different object, but may be same file.
from_page: (int) first source page to copy, 0-based, default 0.
to_page: (int) last source page to copy, 0-based, default last page.
start_at: (int) from_page will become this page number in target.
rotate: (int) rotate copied pages, default -1 is no change.
links: (int/bool) whether to also copy links.
annots: (int/bool) whether to also copy annotations.
show_progress: (int) progress message interval, 0 is no messages.
final: (bool) indicates last insertion from this source PDF.
_gmap: internal use only
Copy sequence reversed if from_page > to_page."""
if self.is_closed or self.is_encrypted:
raise ValueError("document closed or encrypted")
if self._graft_id == docsrc._graft_id:
raise ValueError("source and target cannot be same object")
sa = start_at
if sa < 0:
sa = self.page_count
if len(docsrc) > show_progress > 0:
inname = os.path.basename(docsrc.name)
if not inname:
inname = "memory PDF"
outname = os.path.basename(self.name)
if not outname:
outname = "memory PDF"
print("Inserting '%s' at '%s'" % (inname, outname))
# retrieve / make a Graftmap to avoid duplicate objects
isrt = docsrc._graft_id
_gmap = self.Graftmaps.get(isrt, None)
if _gmap is None:
_gmap = Graftmap(self)
self.Graftmaps[isrt] = _gmap
val = _fitz_old.Document_insert_pdf(self, docsrc, from_page, to_page, start_at, rotate, links, annots, show_progress, final, _gmap)
self._reset_page_refs()
if links:
self._do_links(docsrc, from_page = from_page, to_page = to_page,
start_at = sa)
if final == 1:
self.Graftmaps[isrt] = None
return val
def insert_file(self, infile, from_page=-1, to_page=-1, start_at=-1, rotate=-1, links=True, annots=True,show_progress=0, final=1):
"""Insert an arbitrary supported document to an existing PDF.
The infile may be given as a filename, a Document or a Pixmap.
Other paramters - where applicable - equal those of insert_pdf().
"""
src = None
if isinstance(infile, Pixmap):
if infile.colorspace.n > 3:
infile = Pixmap(csRGB, infile)
src = Document("png", infile.tobytes())
elif isinstance(infile, Document):
src = infile
else:
src = Document(infile)
if not src:
raise ValueError("bad infile parameter")
if not src.is_pdf:
pdfbytes = src.convert_to_pdf()
src = Document("pdf", pdfbytes)
return self.insert_pdf(src, from_page=from_page, to_page=to_page, start_at=start_at, rotate=rotate,links=links, annots=annots, show_progress=show_progress, final=final)
def _newPage(self, pno=-1, width=595, height=842):
"""Make a new PDF page."""
if self.is_closed or self.is_encrypted:
raise ValueError("document closed or encrypted")
val = _fitz_old.Document__newPage(self, pno, width, height)
self._reset_page_refs()
return val
def select(self, pyliste):
"""Build sub-pdf with page numbers in the list."""
if self.is_closed or self.is_encrypted:
raise ValueError("document closed or encrypted")
if not self.is_pdf:
raise ValueError("is no PDF")
if not hasattr(pyliste, "__getitem__"):
raise ValueError("sequence required")
if len(pyliste) == 0 or min(pyliste) not in range(len(self)) or max(pyliste) not in range(len(self)):
raise ValueError("bad page number(s)")
pyliste = tuple(pyliste)
val = _fitz_old.Document_select(self, pyliste)
self._reset_page_refs()
return val
def _delete_page(self, pno):
return _fitz_old.Document__delete_page(self, pno)
@property
def permissions(self):
"""Document permissions."""
if self.is_encrypted:
return 0
return _fitz_old.Document_permissions(self)
def journal_enable(self):
"""Activate document journalling."""
if self.is_closed or self.is_encrypted:
raise ValueError("document closed or encrypted")
return _fitz_old.Document_journal_enable(self)
def journal_start_op(self, name=None):
"""Begin a journalling operation."""
if self.is_closed or self.is_encrypted:
raise ValueError("document closed or encrypted")
return _fitz_old.Document_journal_start_op(self, name)
def journal_stop_op(self):
"""End a journalling operation."""
if self.is_closed or self.is_encrypted:
raise ValueError("document closed or encrypted")
return _fitz_old.Document_journal_stop_op(self)
def journal_position(self):
"""Show journalling state."""
if self.is_closed or self.is_encrypted:
raise ValueError("document closed or encrypted")
return _fitz_old.Document_journal_position(self)
def journal_op_name(self, step):
"""Show operation name for given step."""
if self.is_closed or self.is_encrypted:
raise ValueError("document closed or encrypted")
return _fitz_old.Document_journal_op_name(self, step)
def journal_can_do(self):
"""Show if undo and / or redo are possible."""
if self.is_closed or self.is_encrypted:
raise ValueError("document closed or encrypted")
return _fitz_old.Document_journal_can_do(self)
def journal_undo(self):
"""Move backwards in the journal."""
if self.is_closed or self.is_encrypted:
raise ValueError("document closed or encrypted")
return _fitz_old.Document_journal_undo(self)
def journal_redo(self):
"""Move forward in the journal."""
if self.is_closed or self.is_encrypted:
raise ValueError("document closed or encrypted")
return _fitz_old.Document_journal_redo(self)
def journal_save(self, filename):
"""Save journal to a file."""
if self.is_closed or self.is_encrypted:
raise ValueError("document closed or encrypted")
return _fitz_old.Document_journal_save(self, filename)
def journal_load(self, filename):
"""Load a journal from a file."""
if self.is_closed or self.is_encrypted:
raise ValueError("document closed or encrypted")
return _fitz_old.Document_journal_load(self, filename)
def journal_is_enabled(self):
"""Check if journalling is enabled."""
if self.is_closed or self.is_encrypted:
raise ValueError("document closed or encrypted")
return _fitz_old.Document_journal_is_enabled(self)
def _get_char_widths(self, xref, bfname, ext, ordering, limit, idx=0):
"""Return list of glyphs and glyph widths of a font."""
if self.is_closed or self.is_encrypted:
raise ValueError("document closed or encrypted")
return _fitz_old.Document__get_char_widths(self, xref, bfname, ext, ordering, limit, idx)
def page_xref(self, pno):
"""Get xref of page number."""
if self.is_closed:
raise ValueError("document closed")
return _fitz_old.Document_page_xref(self, pno)
def page_annot_xrefs(self, pno):
"""Get list annotations of page number."""
if self.is_closed:
raise ValueError("document closed")
return _fitz_old.Document_page_annot_xrefs(self, pno)
def page_cropbox(self, pno):
"""Get CropBox of page number (without loading page)."""
if self.is_closed:
raise ValueError("document closed")
val = _fitz_old.Document_page_cropbox(self, pno)
val = Rect(JM_TUPLE3(val))
return val
def _getPageInfo(self, pno, what):
"""List fonts, images, XObjects used on a page."""
if self.is_closed or self.is_encrypted:
raise ValueError("document closed or encrypted")
return _fitz_old.Document__getPageInfo(self, pno, what)
def extract_font(self, xref=0, info_only=0, named=None):
"""Get a font by xref. Returns a tuple or dictionary."""
if self.is_closed or self.is_encrypted:
raise ValueError("document closed or encrypted")
return _fitz_old.Document_extract_font(self, xref, info_only, named)
def extract_image(self, xref):
"""Get image by xref. Returns a dictionary."""
if self.is_closed or self.is_encrypted:
raise ValueError("document closed or encrypted")
return _fitz_old.Document_extract_image(self, xref)
def _delToC(self):
"""Delete the TOC."""
if self.is_closed or self.is_encrypted:
raise ValueError("document closed or encrypted")
val = _fitz_old.Document__delToC(self)
self.init_doc()
return val
def xref_is_stream(self, xref=0):
"""Check if xref is a stream object."""
if self.is_closed:
raise ValueError("document closed")
return _fitz_old.Document_xref_is_stream(self, xref)
def need_appearances(self, value=None):
"""Get/set the NeedAppearances value."""
if self.is_closed:
raise ValueError("document closed")
if not self.is_form_pdf:
return None
return _fitz_old.Document_need_appearances(self, value)
def get_sigflags(self):
"""Get the /SigFlags value."""
if self.is_closed:
raise ValueError("document closed")
return _fitz_old.Document_get_sigflags(self)
@property
def is_form_pdf(self):
"""Either False or PDF field count."""
if self.is_closed:
raise ValueError("document closed")
return _fitz_old.Document_is_form_pdf(self)
@property
def FormFonts(self):
"""Get list of field font resource names."""
if self.is_closed:
raise ValueError("document closed")
return _fitz_old.Document_FormFonts(self)
def _addFormFont(self, name, font):
"""Add new form font."""
if self.is_closed or self.is_encrypted:
raise ValueError("document closed or encrypted")
return _fitz_old.Document__addFormFont(self, name, font)
def _getOLRootNumber(self):
"""Get xref of Outline Root, create it if missing."""
if self.is_closed or self.is_encrypted:
raise ValueError("document closed or encrypted")
return _fitz_old.Document__getOLRootNumber(self)
def get_new_xref(self):
"""Make a new xref."""
if self.is_closed or self.is_encrypted:
raise ValueError("document closed or encrypted")
return _fitz_old.Document_get_new_xref(self)
def xref_length(self):
"""Get length of xref table."""
if self.is_closed:
raise ValueError("document closed")
return _fitz_old.Document_xref_length(self)
def get_xml_metadata(self):
"""Get document XML metadata."""
if self.is_closed:
raise ValueError("document closed")
return _fitz_old.Document_get_xml_metadata(self)
def xref_xml_metadata(self):
"""Get xref of document XML metadata."""
if self.is_closed:
raise ValueError("document closed")
return _fitz_old.Document_xref_xml_metadata(self)
def del_xml_metadata(self):
"""Delete XML metadata."""
if self.is_closed or self.is_encrypted:
raise ValueError("document closed or encrypted")
return _fitz_old.Document_del_xml_metadata(self)
def set_xml_metadata(self, metadata):
"""Store XML document level metadata."""
if self.is_closed or self.is_encrypted:
raise ValueError("document closed or encrypted")
return _fitz_old.Document_set_xml_metadata(self, metadata)
def xref_object(self, xref, compressed=0, ascii=0):
"""Get xref object source as a string."""
if self.is_closed:
raise ValueError("document closed")
return _fitz_old.Document_xref_object(self, xref, compressed, ascii)
def pdf_trailer(self, compressed: bool=False, ascii:bool=False)->str:
"""Get PDF trailer as a string."""
return self.xref_object(-1, compressed=compressed, ascii=ascii)
def xref_stream_raw(self, xref):
"""Get xref stream without decompression."""
if self.is_closed or self.is_encrypted:
raise ValueError("document closed or encrypted")
return _fitz_old.Document_xref_stream_raw(self, xref)
def xref_stream(self, xref):
"""Get decompressed xref stream."""
if self.is_closed or self.is_encrypted:
raise ValueError("document closed or encrypted")
return _fitz_old.Document_xref_stream(self, xref)
def update_object(self, xref, text, page=None):
"""Replace object definition source."""
if self.is_closed or self.is_encrypted:
raise ValueError("document closed or encrypted")
return _fitz_old.Document_update_object(self, xref, text, page)
def update_stream(self, xref=0, stream=None, new=1, compress=1):
"""Replace xref stream part."""
if self.is_closed or self.is_encrypted:
raise ValueError("document closed or encrypted")
return _fitz_old.Document_update_stream(self, xref, stream, new, compress)
def _make_page_map(self):
"""Make an array page number -> page object."""
if self.is_closed:
raise ValueError("document closed")
return _fitz_old.Document__make_page_map(self)
def fullcopy_page(self, pno, to=-1):
"""Make a full page duplicate."""
if self.is_closed:
raise ValueError("document closed")
val = _fitz_old.Document_fullcopy_page(self, pno, to)
self._reset_page_refs()
return val
def _move_copy_page(self, pno, nb, before, copy):
"""Move or copy a PDF page reference."""
if self.is_closed:
raise ValueError("document closed")
val = _fitz_old.Document__move_copy_page(self, pno, nb, before, copy)
self._reset_page_refs()
return val
def _remove_toc_item(self, xref):
return _fitz_old.Document__remove_toc_item(self, xref)
def _update_toc_item(self, xref, action=None, title=None, flags=0, collapse=None, color=None):
return _fitz_old.Document__update_toc_item(self, xref, action, title, flags, collapse, color)
def _get_page_labels(self):
return _fitz_old.Document__get_page_labels(self)
def _set_page_labels(self, labels):
val = _fitz_old.Document__set_page_labels(self, labels)
xref = self.pdf_catalog()
text = self.xref_object(xref, compressed=True)
text = text.replace("/Nums[]", "/Nums[%s]" % labels)
self.update_object(xref, text)
return val
def get_layers(self):
"""Show optional OC layers."""
if self.is_closed:
raise ValueError("document closed")
return _fitz_old.Document_get_layers(self)
def switch_layer(self, config, as_default=0):
"""Activate an OC layer."""
if self.is_closed:
raise ValueError("document closed")
return _fitz_old.Document_switch_layer(self, config, as_default)
def get_layer(self, config=-1):
"""Content of ON, OFF, RBGroups of an OC layer."""
if self.is_closed:
raise ValueError("document closed")
return _fitz_old.Document_get_layer(self, config)
def set_layer(self, config, basestate=None, on=None, off=None, rbgroups=None, locked=None):
"""Set the PDF keys /ON, /OFF, /RBGroups of an OC layer."""
if self.is_closed:
raise ValueError("document closed")
ocgs = set(self.get_ocgs().keys())
if ocgs == set():
raise ValueError("document has no optional content")
if on:
if type(on) not in (list, tuple):
raise ValueError("bad type: 'on'")
s = set(on).difference(ocgs)
if s != set():
raise ValueError("bad OCGs in 'on': %s" % s)
if off:
if type(off) not in (list, tuple):
raise ValueError("bad type: 'off'")
s = set(off).difference(ocgs)
if s != set():
raise ValueError("bad OCGs in 'off': %s" % s)
if locked:
if type(locked) not in (list, tuple):
raise ValueError("bad type: 'locked'")
s = set(locked).difference(ocgs)
if s != set():
raise ValueError("bad OCGs in 'locked': %s" % s)
if rbgroups:
if type(rbgroups) not in (list, tuple):
raise ValueError("bad type: 'rbgroups'")
for x in rbgroups:
if not type(x) in (list, tuple):
raise ValueError("bad RBGroup '%s'" % x)
s = set(x).difference(ocgs)
if s != set():
raise ValueError("bad OCGs in RBGroup: %s" % s)
if basestate:
basestate = str(basestate).upper()
if basestate == "UNCHANGED":
basestate = "Unchanged"
if basestate not in ("ON", "OFF", "Unchanged"):
raise ValueError("bad 'basestate'")
return _fitz_old.Document_set_layer(self, config, basestate, on, off, rbgroups, locked)
def add_layer(self, name, creator=None, on=None):
"""Add a new OC layer."""
if self.is_closed:
raise ValueError("document closed")
return _fitz_old.Document_add_layer(self, name, creator, on)
def layer_ui_configs(self):
"""Show OC visibility status modifiable by user."""
if self.is_closed:
raise ValueError("document closed")
return _fitz_old.Document_layer_ui_configs(self)
def set_layer_ui_config(self, number, action=0):
"""Set / unset OC intent configuration."""
# The user might have given the name instead of sequence number,
# so select by that name and continue with corresp. number
if isinstance(number, str):
select = [ui["number"] for ui in self.layer_ui_configs() if ui["text"] == number]
if select == []:
raise ValueError(f"bad OCG '{number}'.")
number = select[0] # this is the number for the name
return _fitz_old.Document_set_layer_ui_config(self, number, action)
def get_ocgs(self):
"""Show existing optional content groups."""
if self.is_closed:
raise ValueError("document closed")
return _fitz_old.Document_get_ocgs(self)
def add_ocg(self, name, config=-1, on=1, intent=None, usage=None):
"""Add new optional content group."""
if self.is_closed:
raise ValueError("document closed")
return _fitz_old.Document_add_ocg(self, name, config, on, intent, usage)
def internal_keep_annot(self, annot):
return _fitz_old.Document_internal_keep_annot(self, annot)
def init_doc(self):
if self.is_encrypted:
raise ValueError("cannot initialize - document still encrypted")
self._outline = self._loadOutline()
if self._outline:
self._outline.thisown = True
self.metadata = dict([(k,self._getMetadata(v)) for k,v in {'format':'format', 'title':'info:Title', 'author':'info:Author','subject':'info:Subject', 'keywords':'info:Keywords','creator':'info:Creator', 'producer':'info:Producer', 'creationDate':'info:CreationDate', 'modDate':'info:ModDate', 'trapped':'info:Trapped'}.items()])
self.metadata['encryption'] = None if self._getMetadata('encryption')=='None' else self._getMetadata('encryption')
outline = property(lambda self: self._outline)
def get_page_fonts(self, pno: int, full: bool =False) -> list:
"""Retrieve a list of fonts used on a page.
"""
if self.is_closed or self.is_encrypted:
raise ValueError("document closed or encrypted")
if not self.is_pdf:
return ()
if type(pno) is not int:
try:
pno = pno.number
except:
raise ValueError("need a Page or page number")
val = self._getPageInfo(pno, 1)
if full is False:
return [v[:-1] for v in val]
return val
def get_page_images(self, pno: int, full: bool =False) -> list:
"""Retrieve a list of images used on a page.
"""
if self.is_closed or self.is_encrypted:
raise ValueError("document closed or encrypted")
if not self.is_pdf:
return ()
if type(pno) is not int:
try:
pno = pno.number
except:
raise ValueError("need a Page or page number")
val = self._getPageInfo(pno, 2)
if full is False:
return [v[:-1] for v in val]
return val
def get_page_xobjects(self, pno: int) -> list:
"""Retrieve a list of XObjects used on a page.
"""
if self.is_closed or self.is_encrypted:
raise ValueError("document closed or encrypted")
if not self.is_pdf:
return ()
if type(pno) is not int:
try:
pno = pno.number
except:
raise ValueError("need a Page or page number")
val = self._getPageInfo(pno, 3)
rc = [(v[0], v[1], v[2], Rect(v[3])) for v in val]
return rc
def xref_is_image(self, xref):
"""Check if xref is an image object."""
if self.is_closed or self.is_encrypted:
raise ValueError("document closed or encrypted")
if self.xref_get_key(xref, "Subtype")[1] == "/Image":
return True
return False
def xref_is_font(self, xref):
"""Check if xref is a font object."""
if self.is_closed or self.is_encrypted:
raise ValueError("document closed or encrypted")
if self.xref_get_key(xref, "Type")[1] == "/Font":
return True
return False
def xref_is_xobject(self, xref):
"""Check if xref is a form xobject."""
if self.is_closed or self.is_encrypted:
raise ValueError("document closed or encrypted")
if self.xref_get_key(xref, "Subtype")[1] == "/Form":
return True
return False
def copy_page(self, pno: int, to: int =-1):
"""Copy a page within a PDF document.
This will only create another reference of the same page object.
Args:
pno: source page number
to: put before this page, '-1' means after last page.
"""
if self.is_closed:
raise ValueError("document closed")
page_count = len(self)
if (
pno not in range(page_count) or
to not in range(-1, page_count)
):
raise ValueError("bad page number(s)")
before = 1
copy = 1
if to == -1:
to = page_count - 1
before = 0
return self._move_copy_page(pno, to, before, copy)
def move_page(self, pno: int, to: int =-1):
"""Move a page within a PDF document.
Args:
pno: source page number.
to: put before this page, '-1' means after last page.
"""
if self.is_closed:
raise ValueError("document closed")
page_count = len(self)
if (
pno not in range(page_count) or
to not in range(-1, page_count)
):
raise ValueError("bad page number(s)")
before = 1
copy = 0
if to == -1:
to = page_count - 1
before = 0
return self._move_copy_page(pno, to, before, copy)
def delete_page(self, pno: int =-1):
""" Delete one page from a PDF.
"""
if not self.is_pdf:
raise ValueError("is no PDF")
if self.is_closed:
raise ValueError("document closed")
page_count = self.page_count
while pno < 0:
pno += page_count
if pno >= page_count:
raise ValueError("bad page number(s)")
# remove TOC bookmarks pointing to deleted page
toc = self.get_toc()
ol_xrefs = self.get_outline_xrefs()
for i, item in enumerate(toc):
if item[2] == pno + 1:
self._remove_toc_item(ol_xrefs[i])
self._remove_links_to(frozenset((pno,)))
self._delete_page(pno)
self._reset_page_refs()
def delete_pages(self, *args, **kw):
"""Delete pages from a PDF.
Args:
Either keywords 'from_page'/'to_page', or two integers to
specify the first/last page to delete.
Or a list/tuple/range object, which can contain arbitrary
page numbers.
"""
if not self.is_pdf:
raise ValueError("is no PDF")
if self.is_closed:
raise ValueError("document closed")
page_count = self.page_count # page count of document
f = t = -1
if kw: # check if keywords were used
if args: # then no positional args are allowed
raise ValueError("cannot mix keyword and positional argument")
f = kw.get("from_page", -1) # first page to delete
t = kw.get("to_page", -1) # last page to delete
while f < 0:
f += page_count
while t < 0:
t += page_count
if not f <= t < page_count:
raise ValueError("bad page number(s)")
numbers = tuple(range(f, t + 1))
else:
if len(args) > 2 or args == []:
raise ValueError("need 1 or 2 positional arguments")
if len(args) == 2:
f, t = args
if not (type(f) is int and type(t) is int):
raise ValueError("both arguments must be int")
if f > t:
f, t = t, f
if not f <= t < page_count:
raise ValueError("bad page number(s)")
numbers = tuple(range(f, t + 1))
else:
r = args[0]
if type(r) not in (int, range, list, tuple):
raise ValueError("need int or sequence if one argument")
numbers = tuple(r)
numbers = list(map(int, set(numbers))) # ensure unique integers
if numbers == []:
print("nothing to delete")
return
numbers.sort()
if numbers[0] < 0 or numbers[-1] >= page_count:
raise ValueError("bad page number(s)")
frozen_numbers = frozenset(numbers)
toc = self.get_toc()
for i, xref in enumerate(self.get_outline_xrefs()):
if toc[i][2] - 1 in frozen_numbers:
self._remove_toc_item(xref) # remove target in PDF object
self._remove_links_to(frozen_numbers)
for i in reversed(numbers): # delete pages, last to first
self._delete_page(i)
self._reset_page_refs()
def saveIncr(self):
""" Save PDF incrementally"""
return self.save(self.name, incremental=True, encryption=PDF_ENCRYPT_KEEP)
def ez_save(self, filename, garbage=3, clean=False,
deflate=True, deflate_images=True, deflate_fonts=True,
incremental=False, ascii=False, expand=False, linear=False,
pretty=False, encryption=1, permissions=4095,
owner_pw=None, user_pw=None, no_new_id=True):
""" Save PDF using some different defaults"""
return self.save(filename, garbage=garbage,
clean=clean,
deflate=deflate,
deflate_images=deflate_images,
deflate_fonts=deflate_fonts,
incremental=incremental,
ascii=ascii,
expand=expand,
linear=linear,
pretty=pretty,
encryption=encryption,
permissions=permissions,
owner_pw=owner_pw,
user_pw=user_pw,
no_new_id=no_new_id,)
def reload_page(self, page: "struct Page *") -> "struct Page *":
"""Make a fresh copy of a page."""
old_annots = {} # copy annot references to here
pno = page.number # save the page number
for k, v in page._annot_refs.items(): # save the annot dictionary
# We need to call pdf_keep_annot() here, otherwise `v`'s
# refcount can reach zero even if there is an external
# reference.
self.internal_keep_annot(v)
old_annots[k] = v
page._erase() # remove the page
page = None
TOOLS.store_shrink(100)
page = self.load_page(pno) # reload the page
# copy annot refs over to the new dictionary
page_proxy = weakref.proxy(page)
for k, v in old_annots.items():
annot = old_annots[k]
annot.parent = page_proxy # refresh parent to new page
page._annot_refs[k] = annot
return page
@property
def pagemode(self) -> str:
"""Return the PDF PageMode value.
"""
xref = self.pdf_catalog()
if xref == 0:
return None
rc = self.xref_get_key(xref, "PageMode")
if rc[0] == "null":
return "UseNone"
if rc[0] == "name":
return rc[1][1:]
return "UseNone"
def set_pagemode(self, pagemode: str):
"""Set the PDF PageMode value."""
valid = ("UseNone", "UseOutlines", "UseThumbs", "FullScreen", "UseOC", "UseAttachments")
xref = self.pdf_catalog()
if xref == 0:
raise ValueError("not a PDF")
if not pagemode:
raise ValueError("bad PageMode value")
if pagemode[0] == "/":
pagemode = pagemode[1:]
for v in valid:
if pagemode.lower() == v.lower():
self.xref_set_key(xref, "PageMode", f"/{v}")
return True
raise ValueError("bad PageMode value")
@property
def pagelayout(self) -> str:
"""Return the PDF PageLayout value.
"""
xref = self.pdf_catalog()
if xref == 0:
return None
rc = self.xref_get_key(xref, "PageLayout")
if rc[0] == "null":
return "SinglePage"
if rc[0] == "name":
return rc[1][1:]
return "SinglePage"
def set_pagelayout(self, pagelayout: str):
"""Set the PDF PageLayout value."""
valid = ("SinglePage", "OneColumn", "TwoColumnLeft", "TwoColumnRight", "TwoPageLeft", "TwoPageRight")
xref = self.pdf_catalog()
if xref == 0:
raise ValueError("not a PDF")
if not pagelayout:
raise ValueError("bad PageLayout value")
if pagelayout[0] == "/":
pagelayout = pagelayout[1:]
for v in valid:
if pagelayout.lower() == v.lower():
self.xref_set_key(xref, "PageLayout", f"/{v}")
return True
raise ValueError("bad PageLayout value")
@property
def markinfo(self) -> dict:
"""Return the PDF MarkInfo value."""
xref = self.pdf_catalog()
if xref == 0:
return None
rc = self.xref_get_key(xref, "MarkInfo")
if rc[0] == "null":
return {}
if rc[0] == "xref":
xref = int(rc[1].split()[0])
val = self.xref_object(xref, compressed=True)
elif rc[0] == "dict":
val = rc[1]
else:
val = None
if val == None or not (val[:2] == "<<" and val[-2:] == ">>"):
return {}
valid = {"Marked": False, "UserProperties": False, "Suspects": False}
val = val[2:-2].split("/")
for v in val[1:]:
try:
key, value = v.split()
except:
return valid
if value == "true":
valid[key] = True
return valid
def set_markinfo(self, markinfo: dict) -> bool:
"""Set the PDF MarkInfo values."""
xref = self.pdf_catalog()
if xref == 0:
raise ValueError("not a PDF")
if not markinfo or not isinstance(markinfo, dict):
return False
valid = {"Marked": False, "UserProperties": False, "Suspects": False}
if not set(valid.keys()).issuperset(markinfo.keys()):
badkeys = f"bad MarkInfo key(s): {set(markinfo.keys()).difference(valid.keys())}"
raise ValueError(badkeys)
pdfdict = "<<"
valid.update(markinfo)
for key, value in valid.items():
value=str(value).lower()
if not value in ("true", "false"):
raise ValueError(f"bad key value '{key}': '{value}'")
pdfdict += f"/{key} {value}"
pdfdict += ">>"
self.xref_set_key(xref, "MarkInfo", pdfdict)
return True
def __repr__(self) -> str:
m = "closed " if self.is_closed else ""
if self.stream is None:
if self.name == "":
return m + "Document(<new PDF, doc# %i>)" % self._graft_id
return m + "Document('%s')" % (self.name,)
return m + "Document('%s', <memory, doc# %i>)" % (self.name, self._graft_id)
def __contains__(self, loc) -> bool:
if type(loc) is int:
if loc < self.page_count:
return True
return False
if type(loc) not in (tuple, list) or len(loc) != 2:
return False
chapter, pno = loc
if (type(chapter) != int or
chapter < 0 or
chapter >= self.chapter_count
):
return False
if (type(pno) != int or
pno < 0 or
pno >= self.chapter_page_count(chapter)
):
return False
return True
def __getitem__(self, i: int =0)->"Page":
assert isinstance(i, int) or (isinstance(i, tuple) and len(i) == 2 and all(isinstance(x, int) for x in i))
if i not in self:
raise IndexError("page not in document")
return self.load_page(i)
def __delitem__(self, i: AnyType)->None:
if not self.is_pdf:
raise ValueError("is no PDF")
if type(i) is int:
return self.delete_page(i)
if type(i) in (list, tuple, range):
return self.delete_pages(i)
if type(i) is not slice:
raise ValueError("bad argument type")
pc = self.page_count
start = i.start if i.start else 0
stop = i.stop if i.stop else pc
step = i.step if i.step else 1
while start < 0:
start += pc
if start >= pc:
raise ValueError("bad page number(s)")
while stop < 0:
stop += pc
if stop > pc:
raise ValueError("bad page number(s)")
return self.delete_pages(range(start, stop, step))
def pages(self, start: OptInt =None, stop: OptInt =None, step: OptInt =None):
"""Return a generator iterator over a page range.
Arguments have the same meaning as for the range() built-in.
"""
# set the start value
start = start or 0
while start < 0:
start += self.page_count
if start not in range(self.page_count):
raise ValueError("bad start page number")
# set the stop value
stop = stop if stop is not None and stop <= self.page_count else self.page_count
# set the step value
if step == 0:
raise ValueError("arg 3 must not be zero")
if step is None:
if start > stop:
step = -1
else:
step = 1
for pno in range(start, stop, step):
yield (self.load_page(pno))
def __len__(self) -> int:
return self.page_count
def _forget_page(self, page: "struct Page *"):
"""Remove a page from document page dict."""
pid = id(page)
if pid in self._page_refs:
self._page_refs[pid] = None
def _reset_page_refs(self):
"""Invalidate all pages in document dictionary."""
if getattr(self, "is_closed", True):
return
for page in self._page_refs.values():
if page:
page._erase()
page = None
self._page_refs.clear()
def _cleanup(self):
self._reset_page_refs()
for k in self.Graftmaps.keys():
self.Graftmaps[k] = None
self.Graftmaps = {}
self.ShownPages = {}
self.InsertedImages = {}
self.FontInfos = []
self.metadata = None
self.stream = None
self.is_closed = True
def close(self):
"""Close the document."""
if getattr(self, "is_closed", False):
raise ValueError("document closed")
self._cleanup()
if getattr(self, "thisown", False):
self.__swig_destroy__(self)
return
else:
raise RuntimeError("document object unavailable")
def __del__(self):
if not type(self) is Document:
return
self._cleanup()
if getattr(self, "thisown", False):
self.__swig_destroy__(self)
def __enter__(self):
return self
def __exit__(self, *args):
self.close()
# Register Document in _fitz_old:
_fitz_old.Document_swigregister(Document)
class Page(object):
thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag")
def __init__(self, *args, **kwargs):
raise AttributeError("No constructor defined")
__repr__ = _swig_repr
__swig_destroy__ = _fitz_old.delete_Page
def bound(self):
"""Get page rectangle."""
CheckParent(self)
val = _fitz_old.Page_bound(self)
val = Rect(val)
if val.is_infinite and self.parent.is_pdf:
cb = self.cropbox
w, h = cb.width, cb.height
if self.rotation not in (0, 180):
w, h = h, w
val = Rect(0, 0, w, h)
msg = TOOLS.mupdf_warnings(reset=False).splitlines()[-1]
print(msg, file=sys.stderr)
return val
rect = property(bound, doc="page rectangle")
def get_image_bbox(self, name, transform=0):
"""Get rectangle occupied by image 'name'.
'name' is either an item of the image list, or the referencing
name string - elem[7] of the resp. item.
Option 'transform' also returns the image transformation matrix.
"""
CheckParent(self)
doc = self.parent
if doc.is_closed or doc.is_encrypted:
raise ValueError("document closed or encrypted")
inf_rect = Rect(1, 1, -1, -1)
null_mat = Matrix()
if transform:
rc = (inf_rect, null_mat)
else:
rc = inf_rect
if type(name) in (list, tuple):
if not type(name[-1]) is int:
raise ValueError("need item of full page image list")
item = name
else:
imglist = [i for i in doc.get_page_images(self.number, True) if name == i[7]]
if len(imglist) == 1:
item = imglist[0]
elif imglist == []:
raise ValueError("bad image name")
else:
raise ValueError("found multiple images named '%s'." % name)
xref = item[-1]
if xref != 0 or transform == True:
try:
return self.get_image_rects(item, transform=transform)[0]
except:
return inf_rect
val = _fitz_old.Page_get_image_bbox(self, name, transform)
if not bool(val):
return rc
for v in val:
if v[0] != item[-3]:
continue
q = Quad(v[1])
bbox = q.rect
if transform == 0:
rc = bbox
break
hm = Matrix(util_hor_matrix(q.ll, q.lr))
h = abs(q.ll - q.ul)
w = abs(q.ur - q.ul)
m0 = Matrix(1 / w, 0, 0, 1 / h, 0, 0)
m = ~(hm * m0)
rc = (bbox, m)
break
val = rc
return val
def run(self, dw, m):
"""Run page through a device."""
CheckParent(self)
return _fitz_old.Page_run(self, dw, m)
def extend_textpage(self, tpage, flags=0, matrix=None):
return _fitz_old.Page_extend_textpage(self, tpage, flags, matrix)
def _get_textpage(self, clip=None, flags=0, matrix=None):
val = _fitz_old.Page__get_textpage(self, clip, flags, matrix)
val.thisown = True
return val
def get_textpage(self, clip: rect_like = None, flags: int = 0, matrix=None) -> "TextPage":
CheckParent(self)
if matrix is None:
matrix = Matrix(1, 1)
old_rotation = self.rotation
if old_rotation != 0:
self.set_rotation(0)
try:
textpage = self._get_textpage(clip, flags=flags, matrix=matrix)
finally:
if old_rotation != 0:
self.set_rotation(old_rotation)
textpage.parent = weakref.proxy(self)
return textpage
@property
def language(self):
"""Page language."""
return _fitz_old.Page_language(self)
def set_language(self, language=None):
"""Set PDF page default language."""
CheckParent(self)
return _fitz_old.Page_set_language(self, language)
def get_svg_image(self, matrix=None, text_as_path=1):
"""Make SVG image from page."""
CheckParent(self)
return _fitz_old.Page_get_svg_image(self, matrix, text_as_path)
def _set_opacity(self, gstate=None, CA=1, ca=1, blendmode=None):
if CA >= 1 and ca >= 1 and blendmode == None:
return None
tCA = int(round(max(CA , 0) * 100))
if tCA >= 100:
tCA = 99
tca = int(round(max(ca, 0) * 100))
if tca >= 100:
tca = 99
gstate = "fitzca%02i%02i" % (tCA, tca)
return _fitz_old.Page__set_opacity(self, gstate, CA, ca, blendmode)
def _add_caret_annot(self, point):
return _fitz_old.Page__add_caret_annot(self, point)
def _add_redact_annot(self, quad, text=None, da_str=None, align=0, fill=None, text_color=None):
return _fitz_old.Page__add_redact_annot(self, quad, text, da_str, align, fill, text_color)
def _add_line_annot(self, p1, p2):
return _fitz_old.Page__add_line_annot(self, p1, p2)
def _add_text_annot(self, point, text, icon=None):
return _fitz_old.Page__add_text_annot(self, point, text, icon)
def _add_ink_annot(self, list):
return _fitz_old.Page__add_ink_annot(self, list)
def _add_stamp_annot(self, rect, stamp=0):
return _fitz_old.Page__add_stamp_annot(self, rect, stamp)
def _add_file_annot(self, point, buffer, filename, ufilename=None, desc=None, icon=None):
return _fitz_old.Page__add_file_annot(self, point, buffer, filename, ufilename, desc, icon)
def _add_text_marker(self, quads, annot_type):
CheckParent(self)
if not self.parent.is_pdf:
raise ValueError("is no PDF")
val = _fitz_old.Page__add_text_marker(self, quads, annot_type)
if not val:
return None
val.parent = weakref.proxy(self)
self._annot_refs[id(val)] = val
return val
def _add_square_or_circle(self, rect, annot_type):
return _fitz_old.Page__add_square_or_circle(self, rect, annot_type)
def _add_multiline(self, points, annot_type):
return _fitz_old.Page__add_multiline(self, points, annot_type)
def _add_freetext_annot(self, rect, text, fontsize=11, fontname=None, text_color=None, fill_color=None, border_color=None, align=0, rotate=0):
val = _fitz_old.Page__add_freetext_annot(self, rect, text, fontsize, fontname, text_color, fill_color, border_color, align, rotate)
ap = val._getAP()
BT = ap.find(b"BT")
ET = ap.find(b"ET") + 2
ap = ap[BT:ET]
w = rect[2]-rect[0]
h = rect[3]-rect[1]
if rotate in (90, -90, 270):
w, h = h, w
re = b"0 0 %g %g re" % (w, h)
ap = re + b"\nW\nn\n" + ap
ope = None
bwidth = b""
fill_string = ColorCode(fill_color, "f").encode()
if fill_string:
fill_string += b"\n"
ope = b"f"
stroke_string = ColorCode(border_color, "c").encode()
if stroke_string:
stroke_string += b"\n"
bwidth = b"1 w\n"
ope = b"S"
if fill_string and stroke_string:
ope = b"B"
if ope != None:
ap = bwidth + fill_string + stroke_string + re + b"\n" + ope + b"\n" + ap
val._setAP(ap)
return val
@property
def rotation_matrix(self) -> Matrix:
"""Reflects page rotation."""
return Matrix(TOOLS._rotate_matrix(self))
@property
def derotation_matrix(self) -> Matrix:
"""Reflects page de-rotation."""
return Matrix(TOOLS._derotate_matrix(self))
def add_caret_annot(self, point: point_like) -> "struct Annot *":
"""Add a 'Caret' annotation."""
old_rotation = annot_preprocess(self)
try:
annot = self._add_caret_annot(point)
finally:
if old_rotation != 0:
self.set_rotation(old_rotation)
annot_postprocess(self, annot)
return annot
def add_strikeout_annot(self, quads=None, start=None, stop=None, clip=None) -> "struct Annot *":
"""Add a 'StrikeOut' annotation."""
if quads is None:
q = get_highlight_selection(self, start=start, stop=stop, clip=clip)
else:
q = CheckMarkerArg(quads)
return self._add_text_marker(q, PDF_ANNOT_STRIKE_OUT)
def add_underline_annot(self, quads=None, start=None, stop=None, clip=None) -> "struct Annot *":
"""Add a 'Underline' annotation."""
if quads is None:
q = get_highlight_selection(self, start=start, stop=stop, clip=clip)
else:
q = CheckMarkerArg(quads)
return self._add_text_marker(q, PDF_ANNOT_UNDERLINE)
def add_squiggly_annot(self, quads=None, start=None,
stop=None, clip=None) -> "struct Annot *":
"""Add a 'Squiggly' annotation."""
if quads is None:
q = get_highlight_selection(self, start=start, stop=stop, clip=clip)
else:
q = CheckMarkerArg(quads)
return self._add_text_marker(q, PDF_ANNOT_SQUIGGLY)
def add_highlight_annot(self, quads=None, start=None,
stop=None, clip=None) -> "struct Annot *":
"""Add a 'Highlight' annotation."""
if quads is None:
q = get_highlight_selection(self, start=start, stop=stop, clip=clip)
else:
q = CheckMarkerArg(quads)
return self._add_text_marker(q, PDF_ANNOT_HIGHLIGHT)
def add_rect_annot(self, rect: rect_like) -> "struct Annot *":
"""Add a 'Square' (rectangle) annotation."""
old_rotation = annot_preprocess(self)
try:
annot = self._add_square_or_circle(rect, PDF_ANNOT_SQUARE)
finally:
if old_rotation != 0:
self.set_rotation(old_rotation)
annot_postprocess(self, annot)
return annot
def add_circle_annot(self, rect: rect_like) -> "struct Annot *":
"""Add a 'Circle' (ellipse, oval) annotation."""
old_rotation = annot_preprocess(self)
try:
annot = self._add_square_or_circle(rect, PDF_ANNOT_CIRCLE)
finally:
if old_rotation != 0:
self.set_rotation(old_rotation)
annot_postprocess(self, annot)
return annot
def add_text_annot(self, point: point_like, text: str, icon: str ="Note") -> "struct Annot *":
"""Add a 'Text' (sticky note) annotation."""
old_rotation = annot_preprocess(self)
try:
annot = self._add_text_annot(point, text, icon=icon)
finally:
if old_rotation != 0:
self.set_rotation(old_rotation)
annot_postprocess(self, annot)
return annot
def add_line_annot(self, p1: point_like, p2: point_like) -> "struct Annot *":
"""Add a 'Line' annotation."""
old_rotation = annot_preprocess(self)
try:
annot = self._add_line_annot(p1, p2)
finally:
if old_rotation != 0:
self.set_rotation(old_rotation)
annot_postprocess(self, annot)
return annot
def add_polyline_annot(self, points: list) -> "struct Annot *":
"""Add a 'PolyLine' annotation."""
old_rotation = annot_preprocess(self)
try:
annot = self._add_multiline(points, PDF_ANNOT_POLY_LINE)
finally:
if old_rotation != 0:
self.set_rotation(old_rotation)
annot_postprocess(self, annot)
return annot
def add_polygon_annot(self, points: list) -> "struct Annot *":
"""Add a 'Polygon' annotation."""
old_rotation = annot_preprocess(self)
try:
annot = self._add_multiline(points, PDF_ANNOT_POLYGON)
finally:
if old_rotation != 0:
self.set_rotation(old_rotation)
annot_postprocess(self, annot)
return annot
def add_stamp_annot(self, rect: rect_like, stamp: int =0) -> "struct Annot *":
"""Add a ('rubber') 'Stamp' annotation."""
old_rotation = annot_preprocess(self)
try:
annot = self._add_stamp_annot(rect, stamp)
finally:
if old_rotation != 0:
self.set_rotation(old_rotation)
annot_postprocess(self, annot)
return annot
def add_ink_annot(self, handwriting: list) -> "struct Annot *":
"""Add a 'Ink' ('handwriting') annotation.
The argument must be a list of lists of point_likes.
"""
old_rotation = annot_preprocess(self)
try:
annot = self._add_ink_annot(handwriting)
finally:
if old_rotation != 0:
self.set_rotation(old_rotation)
annot_postprocess(self, annot)
return annot
def add_file_annot(self, point: point_like,
buffer: typing.ByteString,
filename: str,
ufilename: OptStr =None,
desc: OptStr =None,
icon: OptStr =None) -> "struct Annot *":
"""Add a 'FileAttachment' annotation."""
old_rotation = annot_preprocess(self)
try:
annot = self._add_file_annot(point,
buffer,
filename,
ufilename=ufilename,
desc=desc,
icon=icon)
finally:
if old_rotation != 0:
self.set_rotation(old_rotation)
annot_postprocess(self, annot)
return annot
def add_freetext_annot(self, rect: rect_like, text: str, fontsize: float =11,
fontname: OptStr =None, border_color: OptSeq =None,
text_color: OptSeq =None,
fill_color: OptSeq =None, align: int =0, rotate: int =0) -> "struct Annot *":
"""Add a 'FreeText' annotation."""
old_rotation = annot_preprocess(self)
try:
annot = self._add_freetext_annot(rect, text, fontsize=fontsize,
fontname=fontname, border_color=border_color,text_color=text_color,
fill_color=fill_color, align=align, rotate=rotate)
finally:
if old_rotation != 0:
self.set_rotation(old_rotation)
annot_postprocess(self, annot)
return annot
def add_redact_annot(self, quad, text: OptStr =None, fontname: OptStr =None,
fontsize: float =11, align: int =0, fill: OptSeq =None, text_color: OptSeq =None,
cross_out: bool =True) -> "struct Annot *":
"""Add a 'Redact' annotation."""
da_str = None
if text:
CheckColor(fill)
CheckColor(text_color)
if not fontname:
fontname = "Helv"
if not fontsize:
fontsize = 11
if not text_color:
text_color = (0, 0, 0)
if hasattr(text_color, "__float__"):
text_color = (text_color, text_color, text_color)
if len(text_color) > 3:
text_color = text_color[:3]
fmt = "{:g} {:g} {:g} rg /{f:s} {s:g} Tf"
da_str = fmt.format(*text_color, f=fontname, s=fontsize)
if fill is None:
fill = (1, 1, 1)
if fill:
if hasattr(fill, "__float__"):
fill = (fill, fill, fill)
if len(fill) > 3:
fill = fill[:3]
old_rotation = annot_preprocess(self)
try:
annot = self._add_redact_annot(quad, text=text, da_str=da_str,
align=align, fill=fill)
finally:
if old_rotation != 0:
self.set_rotation(old_rotation)
annot_postprocess(self, annot)
#-------------------------------------------------------------
# change appearance to show a crossed-out rectangle
#-------------------------------------------------------------
if cross_out:
ap_tab = annot._getAP().splitlines()[:-1] # get the 4 commands only
_, LL, LR, UR, UL = ap_tab
ap_tab.append(LR)
ap_tab.append(LL)
ap_tab.append(UR)
ap_tab.append(LL)
ap_tab.append(UL)
ap_tab.append(b"S")
ap = b"\n".join(ap_tab)
annot._setAP(ap, 0)
return annot
def _load_annot(self, name, xref):
return _fitz_old.Page__load_annot(self, name, xref)
def load_widget(self, xref):
"""Load a widget by its xref."""
CheckParent(self)
val = _fitz_old.Page_load_widget(self, xref)
if not val:
return val
val.thisown = True
val.parent = weakref.proxy(self)
self._annot_refs[id(val)] = val
widget = Widget()
TOOLS._fill_widget(val, widget)
val = widget
return val
def _get_resource_properties(self):
return _fitz_old.Page__get_resource_properties(self)
def _set_resource_property(self, name, xref):
return _fitz_old.Page__set_resource_property(self, name, xref)
def _get_optional_content(self, oc: OptInt) -> OptStr:
if oc == None or oc == 0:
return None
doc = self.parent
check = doc.xref_object(oc, compressed=True)
if not ("/Type/OCG" in check or "/Type/OCMD" in check):
raise ValueError("bad optional content: 'oc'")
props = {}
for p, x in self._get_resource_properties():
props[x] = p
if oc in props.keys():
return props[oc]
i = 0
mc = "MC%i" % i
while mc in props.values():
i += 1
mc = "MC%i" % i
self._set_resource_property(mc, oc)
return mc
def get_oc_items(self) -> list:
"""Get OCGs and OCMDs used in the page's contents.
Returns:
List of items (name, xref, type), where type is one of "ocg" / "ocmd",
and name is the property name.
"""
rc = []
for pname, xref in self._get_resource_properties():
text = self.parent.xref_object(xref, compressed=True)
if "/Type/OCG" in text:
octype = "ocg"
elif "/Type/OCMD" in text:
octype = "ocmd"
else:
continue
rc.append((pname, xref, octype))
return rc
def annot_names(self):
"""List of names of annotations, fields and links."""
CheckParent(self)
return _fitz_old.Page_annot_names(self)
def annot_xrefs(self):
"""List of xref numbers of annotations, fields and links."""
CheckParent(self)
return _fitz_old.Page_annot_xrefs(self)
def load_annot(self, ident: typing.Union[str, int]) -> "struct Annot *":
"""Load an annot by name (/NM key) or xref.
Args:
ident: identifier, either name (str) or xref (int).
"""
CheckParent(self)
if type(ident) is str:
xref = 0
name = ident
elif type(ident) is int:
xref = ident
name = None
else:
raise ValueError("identifier must be string or integer")
val = self._load_annot(name, xref)
if not val:
return val
val.thisown = True
val.parent = weakref.proxy(self)
self._annot_refs[id(val)] = val
return val
#---------------------------------------------------------------------
# page addWidget
#---------------------------------------------------------------------
def add_widget(self, widget: Widget) -> "struct Annot *":
"""Add a 'Widget' (form field)."""
CheckParent(self)
doc = self.parent
if not doc.is_pdf:
raise ValueError("is no PDF")
widget._validate()
annot = self._addWidget(widget.field_type, widget.field_name)
if not annot:
return None
annot.thisown = True
annot.parent = weakref.proxy(self) # owning page object
self._annot_refs[id(annot)] = annot
widget.parent = annot.parent
widget._annot = annot
widget.update()
return annot
def _addWidget(self, field_type, field_name):
return _fitz_old.Page__addWidget(self, field_type, field_name)
def get_displaylist(self, annots=1):
"""Make a DisplayList from the page for Pixmap generation.
Include (default) or exclude annotations."""
CheckParent(self)
val = _fitz_old.Page_get_displaylist(self, annots)
val.thisown = True
return val
def get_drawings(self, extended: bool = False) -> list:
"""Retrieve vector graphics. The extended version includes clips.
Note:
For greater comfort, this method converts point-like, rect-like, quad-like
tuples of the C version to respective Point / Rect / Quad objects.
It also adds default items that are missing in original path types.
"""
allkeys = (
"closePath", "fill", "color", "width", "lineCap",
"lineJoin", "dashes", "stroke_opacity", "fill_opacity", "even_odd",
)
val = self.get_cdrawings(extended=extended)
for i in range(len(val)):
npath = val[i]
if not npath["type"].startswith("clip"):
npath["rect"] = Rect(npath["rect"])
else:
npath["scissor"] = Rect(npath["scissor"])
if npath["type"]!="group":
items = npath["items"]
newitems = []
for item in items:
cmd = item[0]
rest = item[1:]
if cmd == "re":
item = ("re", Rect(rest[0]).normalize(), rest[1])
elif cmd == "qu":
item = ("qu", Quad(rest[0]))
else:
item = tuple([cmd] + [Point(i) for i in rest])
newitems.append(item)
npath["items"] = newitems
if npath["type"] in ("f", "s"):
for k in allkeys:
npath[k] = npath.get(k)
val[i] = npath
return val
class Drawpath(object):
"""Reflects a path dictionary from get_cdrawings()."""
def __init__(self, **args):
self.__dict__.update(args)
class Drawpathlist(object):
"""List of Path objects representing get_cdrawings() output."""
def __init__(self):
self.paths = []
self.path_count = 0
self.group_count = 0
self.clip_count = 0
self.fill_count = 0
self.stroke_count = 0
self.fillstroke_count = 0
def append(self, path):
self.paths.append(path)
self.path_count += 1
if path.type == "clip":
self.clip_count += 1
elif path.type == "group":
self.group_count += 1
elif path.type == "f":
self.fill_count += 1
elif path.type == "s":
self.stroke_count += 1
elif path.type == "fs":
self.fillstroke_count += 1
def clip_parents(self, i):
"""Return list of parent clip paths.
Args:
i: (int) return parents of this path.
Returns:
List of the clip parents."""
if i >= self.path_count:
raise IndexError("bad path index")
while i < 0:
i += self.path_count
lvl = self.paths[i].level
clips = list( # clip paths before identified one
reversed(
[
p
for p in self.paths[:i]
if p.type == "clip" and p.level < lvl
]
)
)
if clips == []: # none found: empty list
return []
nclips = [clips[0]] # init return list
for p in clips[1:]:
if p.level >= nclips[-1].level:
continue # only accept smaller clip levels
nclips.append(p)
return nclips
def group_parents(self, i):
"""Return list of parent group paths.
Args:
i: (int) return parents of this path.
Returns:
List of the group parents."""
if i >= self.path_count:
raise IndexError("bad path index")
while i < 0:
i += self.path_count
lvl = self.paths[i].level
groups = list( # group paths before identified one
reversed(
[
p
for p in self.paths[:i]
if p.type == "group" and p.level < lvl
]
)
)
if groups == []: # none found: empty list
return []
ngroups = [groups[0]] # init return list
for p in groups[1:]:
if p.level >= ngroups[-1].level:
continue # only accept smaller group levels
ngroups.append(p)
return ngroups
def __getitem__(self, item):
return self.paths.__getitem__(item)
def __len__(self):
return self.paths.__len__()
def get_lineart(self) -> object:
"""Get page drawings paths.
Note:
For greater comfort, this method converts point-like, rect-like, quad-like
tuples of the C version to respective Point / Rect / Quad objects.
Also adds default items that are missing in original path types.
In contrast to get_drawings(), this output is an object.
"""
val = self.get_cdrawings(extended=True)
paths = self.Drawpathlist()
for path in val:
npath = self.Drawpath(**path)
if npath.type != "clip":
npath.rect = Rect(path["rect"])
else:
npath.scissor = Rect(path["scissor"])
if npath.type != "group":
items = path["items"]
newitems = []
for item in items:
cmd = item[0]
rest = item[1:]
if cmd == "re":
item = ("re", Rect(rest[0]).normalize(), rest[1])
elif cmd == "qu":
item = ("qu", Quad(rest[0]))
else:
item = tuple([cmd] + [Point(i) for i in rest])
newitems.append(item)
npath.items = newitems
if npath.type == "f":
npath.stroke_opacity = None
npath.dashes = None
npath.lineJoin = None
npath.lineCap = None
npath.color = None
npath.width = None
paths.append(npath)
val = None
return paths
def get_cdrawings(self, extended=None, callback=None, method=None):
"""Extract vector graphics ("line art") from the page."""
CheckParent(self)
old_rotation = self.rotation
if old_rotation != 0:
self.set_rotation(0)
val = _fitz_old.Page_get_cdrawings(self, extended, callback, method)
if old_rotation != 0:
self.set_rotation(old_rotation)
return val
def get_bboxlog(self, layers=None):
CheckParent(self)
old_rotation = self.rotation
if old_rotation != 0:
self.set_rotation(0)
val = _fitz_old.Page_get_bboxlog(self, layers)
if old_rotation != 0:
self.set_rotation(old_rotation)
return val
def get_texttrace(self):
CheckParent(self)
old_rotation = self.rotation
if old_rotation != 0:
self.set_rotation(0)
val = _fitz_old.Page_get_texttrace(self)
if old_rotation != 0:
self.set_rotation(old_rotation)
return val
def _apply_redactions(self, *args):
return _fitz_old.Page__apply_redactions(self, *args)
def _makePixmap(self, doc, ctm, cs, alpha=0, annots=1, clip=None):
return _fitz_old.Page__makePixmap(self, doc, ctm, cs, alpha, annots, clip)
def set_mediabox(self, rect):
"""Set the MediaBox."""
CheckParent(self)
return _fitz_old.Page_set_mediabox(self, rect)
def load_links(self):
"""Get first Link."""
CheckParent(self)
val = _fitz_old.Page_load_links(self)
if val:
val.thisown = True
val.parent = weakref.proxy(self) # owning page object
self._annot_refs[id(val)] = val
if self.parent.is_pdf:
link_id = [x for x in self.annot_xrefs() if x[1] == PDF_ANNOT_LINK][0]
val.xref = link_id[0]
val.id = link_id[2]
else:
val.xref = 0
val.id = ""
return val
first_link = property(load_links, doc="First link on page")
@property
def first_annot(self):
"""First annotation."""
CheckParent(self)
val = _fitz_old.Page_first_annot(self)
if val:
val.thisown = True
val.parent = weakref.proxy(self) # owning page object
self._annot_refs[id(val)] = val
return val
@property
def first_widget(self):
"""First widget/field."""
CheckParent(self)
val = _fitz_old.Page_first_widget(self)
if val:
val.thisown = True
val.parent = weakref.proxy(self) # owning page object
self._annot_refs[id(val)] = val
widget = Widget()
TOOLS._fill_widget(val, widget)
val = widget
return val
def delete_link(self, linkdict):
"""Delete a Link."""
CheckParent(self)
val = _fitz_old.Page_delete_link(self, linkdict)
if linkdict["xref"] == 0: return
try:
linkid = linkdict["id"]
linkobj = self._annot_refs[linkid]
linkobj._erase()
except:
pass
return val
def delete_annot(self, annot):
"""Delete annot and return next one."""
CheckParent(self)
CheckParent(annot)
val = _fitz_old.Page_delete_annot(self, annot)
if val:
val.thisown = True
val.parent = weakref.proxy(self) # owning page object
val.parent._annot_refs[id(val)] = val
return val
@property
def mediabox(self):
"""The MediaBox."""
CheckParent(self)
val = _fitz_old.Page_mediabox(self)
val = Rect(JM_TUPLE3(val))
return val
@property
def cropbox(self):
"""The CropBox."""
CheckParent(self)
val = _fitz_old.Page_cropbox(self)
val = Rect(JM_TUPLE3(val))
return val
def _other_box(self, boxtype):
return _fitz_old.Page__other_box(self, boxtype)
@property
def cropbox_position(self):
return self.cropbox.tl
@property
def artbox(self):
"""The ArtBox"""
rect = self._other_box("ArtBox")
if rect == None:
return self.cropbox
mb = self.mediabox
return Rect(rect[0], mb.y1 - rect[3], rect[2], mb.y1 - rect[1])
@property
def trimbox(self):
"""The TrimBox"""
rect = self._other_box("TrimBox")
if rect == None:
return self.cropbox
mb = self.mediabox
return Rect(rect[0], mb.y1 - rect[3], rect[2], mb.y1 - rect[1])
@property
def bleedbox(self):
"""The BleedBox"""
rect = self._other_box("BleedBox")
if rect == None:
return self.cropbox
mb = self.mediabox
return Rect(rect[0], mb.y1 - rect[3], rect[2], mb.y1 - rect[1])
def _set_pagebox(self, boxtype, rect):
doc = self.parent
if doc == None:
raise ValueError("orphaned object: parent is None")
if not doc.is_pdf:
raise ValueError("is no PDF")
valid_boxes = ("CropBox", "BleedBox", "TrimBox", "ArtBox")
if boxtype not in valid_boxes:
raise ValueError("bad boxtype")
rect = Rect(rect)
mb = self.mediabox
rect = Rect(rect[0], mb.y1 - rect[3], rect[2], mb.y1 - rect[1])
if not (mb.x0 <= rect.x0 < rect.x1 <= mb.x1 and mb.y0 <= rect.y0 < rect.y1 <= mb.y1):
raise ValueError(f"{boxtype} not in MediaBox")
doc.xref_set_key(self.xref, boxtype, "[%g %g %g %g]" % tuple(rect))
def set_cropbox(self, rect):
"""Set the CropBox. Will also change Page.rect."""
return self._set_pagebox("CropBox", rect)
def set_artbox(self, rect):
"""Set the ArtBox."""
return self._set_pagebox("ArtBox", rect)
def set_bleedbox(self, rect):
"""Set the BleedBox."""
return self._set_pagebox("BleedBox", rect)
def set_trimbox(self, rect):
"""Set the TrimBox."""
return self._set_pagebox("TrimBox", rect)
@property
def rotation(self):
"""Page rotation."""
CheckParent(self)
return _fitz_old.Page_rotation(self)
def set_rotation(self, rotation):
"""Set page rotation."""
CheckParent(self)
return _fitz_old.Page_set_rotation(self, rotation)
def _addAnnot_FromString(self, linklist):
"""Add links from list of object sources."""
CheckParent(self)
return _fitz_old.Page__addAnnot_FromString(self, linklist)
def clean_contents(self, sanitize=1):
"""Clean page /Contents into one object."""
CheckParent(self)
if not sanitize and not self.is_wrapped:
self.wrap_contents()
return _fitz_old.Page_clean_contents(self, sanitize)
def _show_pdf_page(self, fz_srcpage, overlay=1, matrix=None, xref=0, oc=0, clip=None, graftmap=None, _imgname=None):
return _fitz_old.Page__show_pdf_page(self, fz_srcpage, overlay, matrix, xref, oc, clip, graftmap, _imgname)
def _insert_image(self, filename=None, pixmap=None, stream=None, imask=None, clip=None, overlay=1, rotate=0, keep_proportion=1, oc=0, width=0, height=0, xref=0, alpha=-1, _imgname=None, digests=None):
return _fitz_old.Page__insert_image(self, filename, pixmap, stream, imask, clip, overlay, rotate, keep_proportion, oc, width, height, xref, alpha, _imgname, digests)
def refresh(self):
doc = self.parent
page = doc.reload_page(self)
self = page
def insert_font(self, fontname="helv", fontfile=None, fontbuffer=None,
set_simple=False, wmode=0, encoding=0):
doc = self.parent
if doc is None:
raise ValueError("orphaned object: parent is None")
idx = 0
if fontname.startswith("/"):
fontname = fontname[1:]
inv_chars = INVALID_NAME_CHARS.intersection(fontname)
if inv_chars != set():
raise ValueError(f"bad fontname chars {inv_chars}")
font = CheckFont(self, fontname)
if font is not None: # font already in font list of page
xref = font[0] # this is the xref
if CheckFontInfo(doc, xref): # also in our document font list?
return xref # yes: we are done
# need to build the doc FontInfo entry - done via get_char_widths
doc.get_char_widths(xref)
return xref
#--------------------------------------------------------------------------
# the font is not present for this page
#--------------------------------------------------------------------------
bfname = Base14_fontdict.get(fontname.lower(), None) # BaseFont if Base-14 font
serif = 0
CJK_number = -1
CJK_list_n = ["china-t", "china-s", "japan", "korea"]
CJK_list_s = ["china-ts", "china-ss", "japan-s", "korea-s"]
try:
CJK_number = CJK_list_n.index(fontname)
serif = 0
except:
pass
if CJK_number < 0:
try:
CJK_number = CJK_list_s.index(fontname)
serif = 1
except:
pass
if fontname.lower() in fitz_fontdescriptors.keys():
import pymupdf_fonts
fontbuffer = pymupdf_fonts.myfont(fontname) # make a copy
del pymupdf_fonts
# install the font for the page
if fontfile != None:
if type(fontfile) is str:
fontfile_str = fontfile
elif hasattr(fontfile, "absolute"):
fontfile_str = str(fontfile)
elif hasattr(fontfile, "name"):
fontfile_str = fontfile.name
else:
raise ValueError("bad fontfile")
else:
fontfile_str = None
val = self._insertFont(fontname, bfname, fontfile_str, fontbuffer, set_simple, idx,
wmode, serif, encoding, CJK_number)
if not val: # did not work, error return
return val
xref = val[0] # xref of installed font
fontdict = val[1]
if CheckFontInfo(doc, xref): # check again: document already has this font
return xref # we are done
# need to create document font info
doc.get_char_widths(xref, fontdict=fontdict)
return xref
def _insertFont(self, fontname, bfname, fontfile, fontbuffer, set_simple, idx, wmode, serif, encoding, ordering):
return _fitz_old.Page__insertFont(self, fontname, bfname, fontfile, fontbuffer, set_simple, idx, wmode, serif, encoding, ordering)
@property
def transformation_matrix(self):
"""Page transformation matrix."""
CheckParent(self)
val = _fitz_old.Page_transformation_matrix(self)
if self.rotation % 360 == 0:
val = Matrix(val)
else:
val = Matrix(1, 0, 0, -1, 0, self.cropbox.height)
return val
def get_contents(self):
"""Get xrefs of /Contents objects."""
CheckParent(self)
return _fitz_old.Page_get_contents(self)
def set_contents(self, xref: int)->None:
"""Set object at 'xref' as the page's /Contents."""
CheckParent(self)
doc = self.parent
if doc.is_closed:
raise ValueError("document closed")
if not doc.is_pdf:
raise ValueError("is no PDF")
if not xref in range(1, doc.xref_length()):
raise ValueError("bad xref")
if not doc.xref_is_stream(xref):
raise ValueError("xref is no stream")
doc.xref_set_key(self.xref, "Contents", "%i 0 R" % xref)
@property
def is_wrapped(self):
"""Check if /Contents is wrapped with string pair "q" / "Q"."""
if getattr(self, "was_wrapped", False): # costly checks only once
return True
cont = self.read_contents().split()
if cont == []: # no contents treated as okay
self.was_wrapped = True
return True
if cont[0] != b"q" or cont[-1] != b"Q":
return False # potential "geometry" issue
self.was_wrapped = True # cheap check next time
return True
def wrap_contents(self):
if self.is_wrapped: # avoid unnecessary wrapping
return
TOOLS._insert_contents(self, b"q\n", False)
TOOLS._insert_contents(self, b"\nQ", True)
self.was_wrapped = True # indicate not needed again
def links(self, kinds=None):
""" Generator over the links of a page.
Args:
kinds: (list) link kinds to subselect from. If none,
all links are returned. E.g. kinds=[LINK_URI]
will only yield URI links.
"""
all_links = self.get_links()
for link in all_links:
if kinds is None or link["kind"] in kinds:
yield (link)
def annots(self, types=None):
""" Generator over the annotations of a page.
Args:
types: (list) annotation types to subselect from. If none,
all annotations are returned. E.g. types=[PDF_ANNOT_LINE]
will only yield line annotations.
"""
skip_types = (PDF_ANNOT_LINK, PDF_ANNOT_POPUP, PDF_ANNOT_WIDGET)
if not hasattr(types, "__getitem__"):
annot_xrefs = [a[0] for a in self.annot_xrefs() if a[1] not in skip_types]
else:
annot_xrefs = [a[0] for a in self.annot_xrefs() if a[1] in types and a[1] not in skip_types]
for xref in annot_xrefs:
annot = self.load_annot(xref)
annot._yielded=True
yield annot
def widgets(self, types=None):
""" Generator over the widgets of a page.
Args:
types: (list) field types to subselect from. If none,
all fields are returned. E.g. types=[PDF_WIDGET_TYPE_TEXT]
will only yield text fields.
"""
widget_xrefs = [a[0] for a in self.annot_xrefs() if a[1] == PDF_ANNOT_WIDGET]
for xref in widget_xrefs:
widget = self.load_widget(xref)
if types == None or widget.field_type in types:
yield (widget)
def __str__(self):
CheckParent(self)
x = self.parent.name
if self.parent.stream is not None:
x = "<memory, doc# %i>" % (self.parent._graft_id,)
if x == "":
x = "<new PDF, doc# %i>" % self.parent._graft_id
return "page %s of %s" % (self.number, x)
def __repr__(self):
CheckParent(self)
x = self.parent.name
if self.parent.stream is not None:
x = "<memory, doc# %i>" % (self.parent._graft_id,)
if x == "":
x = "<new PDF, doc# %i>" % self.parent._graft_id
return "page %s of %s" % (self.number, x)
def _reset_annot_refs(self):
"""Invalidate / delete all annots of this page."""
for annot in self._annot_refs.values():
if annot:
annot._erase()
self._annot_refs.clear()
@property
def xref(self):
"""PDF xref number of page."""
CheckParent(self)
return self.parent.page_xref(self.number)
def _erase(self):
self._reset_annot_refs()
self._image_infos = None
try:
self.parent._forget_page(self)
except:
pass
if getattr(self, "thisown", False):
self.__swig_destroy__(self)
self.parent = None
self.number = None
def __del__(self):
self._erase()
def get_fonts(self, full=False):
"""List of fonts defined in the page object."""
CheckParent(self)
return self.parent.get_page_fonts(self.number, full=full)
def get_images(self, full=False):
"""List of images defined in the page object."""
CheckParent(self)
ret = self.parent.get_page_images(self.number, full=full)
return ret
def get_xobjects(self):
"""List of xobjects defined in the page object."""
CheckParent(self)
return self.parent.get_page_xobjects(self.number)
def read_contents(self):
"""All /Contents streams concatenated to one bytes object."""
return TOOLS._get_all_contents(self)
@property
def mediabox_size(self):
return Point(self.mediabox.x1, self.mediabox.y1)
# Register Page in _fitz_old:
_fitz_old.Page_swigregister(Page)
class Pixmap(object):
thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag")
__repr__ = _swig_repr
__swig_destroy__ = _fitz_old.delete_Pixmap
def __init__(self, *args):
"""Pixmap(colorspace, irect, alpha) - empty pixmap.
Pixmap(colorspace, src) - copy changing colorspace.
Pixmap(src, width, height,[clip]) - scaled copy, float dimensions.
Pixmap(src, alpha=True) - copy adding / dropping alpha.
Pixmap(source, mask) - from a non-alpha and a mask pixmap.
Pixmap(file) - from an image file.
Pixmap(memory) - from an image in memory (bytes).
Pixmap(colorspace, width, height, samples, alpha) - from samples data.
Pixmap(PDFdoc, xref) - from an image xref in a PDF document.
"""
_fitz_old.Pixmap_swiginit(self, _fitz_old.new_Pixmap(*args))
def warp(self, quad, width, height):
"""Return pixmap from a warped quad."""
EnsureOwnership(self)
if not quad.is_convex: raise ValueError("quad must be convex")
return _fitz_old.Pixmap_warp(self, quad, width, height)
def shrink(self, factor):
"""Divide width and height by 2**factor.
E.g. factor=1 shrinks to 25% of original size (in place)."""
EnsureOwnership(self)
return _fitz_old.Pixmap_shrink(self, factor)
def gamma_with(self, gamma):
"""Apply correction with some float.
gamma=1 is a no-op."""
EnsureOwnership(self)
return _fitz_old.Pixmap_gamma_with(self, gamma)
def tint_with(self, black, white):
"""Tint colors with modifiers for black and white."""
EnsureOwnership(self)
if not self.colorspace or self.colorspace.n > 3:
print("warning: colorspace invalid for function")
return
return _fitz_old.Pixmap_tint_with(self, black, white)
def clear_with(self, *args):
"""Fill all color components with same value."""
EnsureOwnership(self)
return _fitz_old.Pixmap_clear_with(self, *args)
def copy(self, src, bbox):
"""Copy bbox from another Pixmap."""
EnsureOwnership(self)
return _fitz_old.Pixmap_copy(self, src, bbox)
def set_alpha(self, alphavalues=None, premultiply=1, opaque=None, matte=None):
"""Set alpha channel to values contained in a byte array.
If None, all alphas are 255.
Args:
alphavalues: (bytes) with length (width * height) or 'None'.
premultiply: (bool, True) premultiply colors with alpha values.
opaque: (tuple, length colorspace.n) this color receives opacity 0.
matte: (tuple, length colorspace.n) preblending background color.
"""
EnsureOwnership(self)
return _fitz_old.Pixmap_set_alpha(self, alphavalues, premultiply, opaque, matte)
def _tobytes(self, format, jpg_quality):
return _fitz_old.Pixmap__tobytes(self, format, jpg_quality)
def tobytes(self, output="png", jpg_quality=95):
"""Convert to binary image stream of desired type.
Can be used as input to GUI packages like tkinter.
Args:
output: (str) image type, default is PNG. Others are JPG, JPEG, PNM, PGM, PPM,
PBM, PAM, PSD, PS.
Returns:
Bytes object.
"""
EnsureOwnership(self)
valid_formats = {"png": 1, "pnm": 2, "pgm": 2, "ppm": 2, "pbm": 2,
"pam": 3, "psd": 5, "ps": 6, "jpg": 7, "jpeg": 7}
idx = valid_formats.get(output.lower(), None)
if idx==None:
raise ValueError(f"Image format {output} not in {tuple(valid_formats.keys())}")
if self.alpha and idx in (2, 6, 7):
raise ValueError("'%s' cannot have alpha" % output)
if self.colorspace and self.colorspace.n > 3 and idx in (1, 2, 4):
raise ValueError("unsupported colorspace for '%s'" % output)
if idx == 7:
self.set_dpi(self.xres, self.yres)
barray = self._tobytes(idx, jpg_quality)
return barray
def pdfocr_save(self, filename, compress=1, language=None, tessdata=None):
EnsureOwnership(self)
return _fitz_old.Pixmap_pdfocr_save(self, filename, compress, language, tessdata)
def pdfocr_tobytes(self, compress=True, language="eng", tessdata=None):
"""Save pixmap as an OCR-ed PDF page.
Args:
compress: (bool) compress, default 1 (True).
language: (str) language(s) occurring on page, default "eng" (English),
multiples like "eng+ger" for English and German.
tessdata: (str) folder name of Tesseract's language support. Must be
given if environment variable TESSDATA_PREFIX is not set.
Notes:
On failure, make sure Tesseract is installed and you have set the
environment variable "TESSDATA_PREFIX" to the folder containing your
Tesseract's language support data.
"""
if not os.getenv("TESSDATA_PREFIX") and not tessdata:
raise RuntimeError("No OCR support: TESSDATA_PREFIX not set")
EnsureOwnership(self)
from io import BytesIO
bio = BytesIO()
self.pdfocr_save(bio, compress=compress, language=language, tessdata=tessdata)
return bio.getvalue()
def _writeIMG(self, filename, format, jpg_quality):
return _fitz_old.Pixmap__writeIMG(self, filename, format, jpg_quality)
def save(self, filename, output=None, jpg_quality=95):
"""Output as image in format determined by filename extension.
Args:
output: (str) only use to overrule filename extension. Default is PNG.
Others are JPEG, JPG, PNM, PGM, PPM, PBM, PAM, PSD, PS.
"""
EnsureOwnership(self)
valid_formats = {"png": 1, "pnm": 2, "pgm": 2, "ppm": 2, "pbm": 2,
"pam": 3, "psd": 5, "ps": 6, "jpg": 7, "jpeg": 7}
if type(filename) is str:
pass
elif hasattr(filename, "absolute"):
filename = str(filename)
elif hasattr(filename, "name"):
filename = filename.name
if output is None:
_, ext = os.path.splitext(filename)
output = ext[1:]
idx = valid_formats.get(output.lower(), None)
if idx == None:
raise ValueError(f"Image format {output} not in {tuple(valid_formats.keys())}")
if self.alpha and idx in (2, 6, 7):
raise ValueError("'%s' cannot have alpha" % output)
if self.colorspace and self.colorspace.n > 3 and idx in (1, 2, 4):
raise ValueError("unsupported colorspace for '%s'" % output)
if idx == 7:
self.set_dpi(self.xres, self.yres)
return self._writeIMG(filename, idx, jpg_quality)
def pil_save(self, *args, unmultiply=False, **kwargs):
"""Write to image file using Pillow.
Args are passed to Pillow's Image.save method, see their documentation.
Use instead of save when other output formats are desired.
:arg bool unmultiply: generates Pillow mode "RGBa" instead of "RGBA".
Relevant for colorspace RGB with alpha only.
"""
EnsureOwnership(self)
try:
from PIL import Image
except ImportError:
print("Pillow not installed")
raise
cspace = self.colorspace
if cspace is None:
mode = "L"
elif cspace.n == 1:
mode = "L" if self.alpha == 0 else "LA"
elif cspace.n == 3:
mode = "RGB" if self.alpha == 0 else "RGBA"
if mode == "RGBA" and unmultiply:
mode = "RGBa"
else:
mode = "CMYK"
img = Image.frombytes(mode, (self.width, self.height), self.samples)
if "dpi" not in kwargs.keys():
kwargs["dpi"] = (self.xres, self.yres)
img.save(*args, **kwargs)
def pil_tobytes(self, *args, unmultiply=False, **kwargs):
"""Convert to binary image stream using pillow.
Args are passed to Pillow's Image.save method, see their documentation.
Use instead of 'tobytes' when other output formats are needed.
"""
EnsureOwnership(self)
from io import BytesIO
bytes_out = BytesIO()
self.pil_save(bytes_out, *args, unmultiply=unmultiply, **kwargs)
return bytes_out.getvalue()
def invert_irect(self, bbox=None):
"""Invert the colors inside a bbox."""
return _fitz_old.Pixmap_invert_irect(self, bbox)
def pixel(self, x, y):
"""Get color tuple of pixel (x, y).
Includes alpha byte if applicable."""
EnsureOwnership(self)
return _fitz_old.Pixmap_pixel(self, x, y)
def set_pixel(self, x, y, color):
"""Set color of pixel (x, y)."""
EnsureOwnership(self)
return _fitz_old.Pixmap_set_pixel(self, x, y, color)
def set_origin(self, x, y):
"""Set top-left coordinates."""
EnsureOwnership(self)
return _fitz_old.Pixmap_set_origin(self, x, y)
def set_dpi(self, xres, yres):
"""Set resolution in both dimensions."""
EnsureOwnership(self)
return _fitz_old.Pixmap_set_dpi(self, xres, yres)
def set_rect(self, bbox, color):
"""Set color of all pixels in bbox."""
EnsureOwnership(self)
return _fitz_old.Pixmap_set_rect(self, bbox, color)
@property
def is_monochrome(self):
"""Check if pixmap is monochrome."""
EnsureOwnership(self)
return _fitz_old.Pixmap_is_monochrome(self)
@property
def is_unicolor(self):
"""Check if pixmap has only one color."""
EnsureOwnership(self)
return _fitz_old.Pixmap_is_unicolor(self)
def color_count(self, colors=0, clip=None):
"""Return count of each color."""
EnsureOwnership(self)
return _fitz_old.Pixmap_color_count(self, colors, clip)
def color_topusage(self, clip=None):
"""Return most frequent color and its usage ratio."""
EnsureOwnership(self)
allpixels = 0
cnt = 0
if clip != None and self.irect in Rect(clip):
clip = self.irect
for pixel, count in self.color_count(colors=True,clip=clip).items():
allpixels += count
if count > cnt:
cnt = count
maxpixel = pixel
if not allpixels:
return (1, bytes([255] * self.n))
return (cnt / allpixels, maxpixel)
@property
def digest(self):
"""MD5 digest of pixmap (bytes)."""
EnsureOwnership(self)
return _fitz_old.Pixmap_digest(self)
@property
def stride(self):
"""Length of one image line (width * n)."""
EnsureOwnership(self)
return _fitz_old.Pixmap_stride(self)
@property
def xres(self):
"""Resolution in x direction."""
EnsureOwnership(self)
return _fitz_old.Pixmap_xres(self)
@property
def yres(self):
"""Resolution in y direction."""
EnsureOwnership(self)
return _fitz_old.Pixmap_yres(self)
@property
def w(self):
"""The width."""
EnsureOwnership(self)
return _fitz_old.Pixmap_w(self)
@property
def h(self):
"""The height."""
EnsureOwnership(self)
return _fitz_old.Pixmap_h(self)
@property
def x(self):
"""x component of Pixmap origin."""
EnsureOwnership(self)
return _fitz_old.Pixmap_x(self)
@property
def y(self):
"""y component of Pixmap origin."""
EnsureOwnership(self)
return _fitz_old.Pixmap_y(self)
@property
def n(self):
"""The size of one pixel."""
EnsureOwnership(self)
return _fitz_old.Pixmap_n(self)
@property
def alpha(self):
"""Indicates presence of alpha channel."""
EnsureOwnership(self)
return _fitz_old.Pixmap_alpha(self)
@property
def colorspace(self):
"""Pixmap Colorspace."""
EnsureOwnership(self)
return _fitz_old.Pixmap_colorspace(self)
@property
def irect(self):
"""Pixmap bbox - an IRect object."""
EnsureOwnership(self)
val = _fitz_old.Pixmap_irect(self)
val = IRect(val)
return val
@property
def size(self):
"""Pixmap size."""
EnsureOwnership(self)
return _fitz_old.Pixmap_size(self)
@property
def samples_mv(self):
"""Pixmap samples memoryview."""
EnsureOwnership(self)
return _fitz_old.Pixmap_samples_mv(self)
@property
def samples_ptr(self):
"""Pixmap samples pointer."""
EnsureOwnership(self)
return _fitz_old.Pixmap_samples_ptr(self)
@property
def samples(self)->bytes:
return bytes(self.samples_mv)
width = w
height = h
def __len__(self):
return self.size
def __repr__(self):
EnsureOwnership(self)
if not type(self) is Pixmap: return
if self.colorspace:
return "Pixmap(%s, %s, %s)" % (self.colorspace.name, self.irect, self.alpha)
else:
return "Pixmap(%s, %s, %s)" % ('None', self.irect, self.alpha)
def __enter__(self):
return self
def __exit__(self, *args):
if getattr(self, "thisown", False):
self.__swig_destroy__(self)
def __del__(self):
if not type(self) is Pixmap:
return
if getattr(self, "thisown", False):
self.__swig_destroy__(self)
# Register Pixmap in _fitz_old:
_fitz_old.Pixmap_swigregister(Pixmap)
class Colorspace(object):
thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag")
__repr__ = _swig_repr
__swig_destroy__ = _fitz_old.delete_Colorspace
def __init__(self, type):
"""Supported are GRAY, RGB and CMYK."""
_fitz_old.Colorspace_swiginit(self, _fitz_old.new_Colorspace(type))
@property
def n(self):
"""Size of one pixel."""
return _fitz_old.Colorspace_n(self)
def _name(self):
return _fitz_old.Colorspace__name(self)
@property
def name(self):
"""Name of the Colorspace."""
if self.n == 1:
return csGRAY._name()
elif self.n == 3:
return csRGB._name()
elif self.n == 4:
return csCMYK._name()
return self._name()
def __repr__(self):
x = ("", "GRAY", "", "RGB", "CMYK")[self.n]
return "Colorspace(CS_%s) - %s" % (x, self.name)
# Register Colorspace in _fitz_old:
_fitz_old.Colorspace_swigregister(Colorspace)
class Device(object):
thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag")
__repr__ = _swig_repr
def __init__(self, *args):
_fitz_old.Device_swiginit(self, _fitz_old.new_Device(*args))
__swig_destroy__ = _fitz_old.delete_Device
# Register Device in _fitz_old:
_fitz_old.Device_swigregister(Device)
class Outline(object):
thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag")
def __init__(self, *args, **kwargs):
raise AttributeError("No constructor defined")
__repr__ = _swig_repr
__swig_destroy__ = _fitz_old.delete_Outline
@property
def uri(self):
return _fitz_old.Outline_uri(self)
@property
def next(self):
return _fitz_old.Outline_next(self)
@property
def down(self):
return _fitz_old.Outline_down(self)
@property
def is_external(self):
return _fitz_old.Outline_is_external(self)
@property
def page(self):
return _fitz_old.Outline_page(self)
@property
def x(self):
return _fitz_old.Outline_x(self)
@property
def y(self):
return _fitz_old.Outline_y(self)
@property
def title(self):
return _fitz_old.Outline_title(self)
@property
def is_open(self):
return _fitz_old.Outline_is_open(self)
@property
def dest(self):
'''outline destination details'''
return linkDest(self, None)
def __del__(self):
if not isinstance(self, Outline):
return
if getattr(self, "thisown", False):
self.__swig_destroy__(self)
# Register Outline in _fitz_old:
_fitz_old.Outline_swigregister(Outline)
class Annot(object):
thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag")
def __init__(self, *args, **kwargs):
raise AttributeError("No constructor defined")
__repr__ = _swig_repr
__swig_destroy__ = _fitz_old.delete_Annot
@property
def rect(self):
"""annotation rectangle"""
CheckParent(self)
val = _fitz_old.Annot_rect(self)
val = Rect(val)
val *= self.parent.derotation_matrix
return val
@property
def rect_delta(self):
"""annotation delta values to rectangle"""
CheckParent(self)
return _fitz_old.Annot_rect_delta(self)
@property
def xref(self):
"""annotation xref"""
CheckParent(self)
return _fitz_old.Annot_xref(self)
@property
def irt_xref(self):
"""annotation IRT xref"""
CheckParent(self)
return _fitz_old.Annot_irt_xref(self)
def set_irt_xref(self, xref):
"""Set annotation IRT xref"""
CheckParent(self)
return _fitz_old.Annot_set_irt_xref(self, xref)
@property
def apn_matrix(self):
"""annotation appearance matrix"""
CheckParent(self)
val = _fitz_old.Annot_apn_matrix(self)
val = Matrix(val)
return val
@property
def apn_bbox(self):
"""annotation appearance bbox"""
CheckParent(self)
val = _fitz_old.Annot_apn_bbox(self)
val = Rect(val) * self.parent.transformation_matrix
val *= self.parent.derotation_matrix
return val
def set_apn_matrix(self, matrix):
"""Set annotation appearance matrix."""
CheckParent(self)
return _fitz_old.Annot_set_apn_matrix(self, matrix)
def set_apn_bbox(self, bbox):
"""Set annotation appearance bbox."""
CheckParent(self)
page = self.parent
rot = page.rotation_matrix
mat = page.transformation_matrix
bbox *= rot * ~mat
return _fitz_old.Annot_set_apn_bbox(self, bbox)
@property
def blendmode(self):
"""annotation BlendMode"""
CheckParent(self)
return _fitz_old.Annot_blendmode(self)
def set_blendmode(self, blend_mode):
"""Set annotation BlendMode."""
CheckParent(self)
return _fitz_old.Annot_set_blendmode(self, blend_mode)
def get_oc(self):
"""Get annotation optional content reference."""
CheckParent(self)
return _fitz_old.Annot_get_oc(self)
def set_open(self, is_open):
"""Set 'open' status of annotation or its Popup."""
CheckParent(self)
return _fitz_old.Annot_set_open(self, is_open)
@property
def is_open(self):
"""Get 'open' status of annotation or its Popup."""
CheckParent(self)
return _fitz_old.Annot_is_open(self)
@property
def has_popup(self):
"""Check if annotation has a Popup."""
CheckParent(self)
return _fitz_old.Annot_has_popup(self)
def set_popup(self, rect):
"""Create annotation 'Popup' or update rectangle."""
CheckParent(self)
return _fitz_old.Annot_set_popup(self, rect)
@property
def popup_rect(self):
"""annotation 'Popup' rectangle"""
CheckParent(self)
val = _fitz_old.Annot_popup_rect(self)
val = Rect(val) * self.parent.transformation_matrix
val *= self.parent.derotation_matrix
return val
@property
def popup_xref(self):
"""annotation 'Popup' xref"""
CheckParent(self)
return _fitz_old.Annot_popup_xref(self)
def set_oc(self, oc=0):
"""Set / remove annotation OC xref."""
CheckParent(self)
return _fitz_old.Annot_set_oc(self, oc)
@property
def language(self):
"""annotation language"""
return _fitz_old.Annot_language(self)
def set_language(self, language=None):
"""Set annotation language."""
CheckParent(self)
return _fitz_old.Annot_set_language(self, language)
def _getAP(self):
return _fitz_old.Annot__getAP(self)
def _setAP(self, buffer, rect=0):
return _fitz_old.Annot__setAP(self, buffer, rect)
def _get_redact_values(self):
val = _fitz_old.Annot__get_redact_values(self)
if not val:
return val
val["rect"] = self.rect
text_color, fontname, fontsize = TOOLS._parse_da(self)
val["text_color"] = text_color
val["fontname"] = fontname
val["fontsize"] = fontsize
fill = self.colors["fill"]
val["fill"] = fill
return val
def get_textpage(self, clip=None, flags=0):
"""Make annotation TextPage."""
CheckParent(self)
val = _fitz_old.Annot_get_textpage(self, clip, flags)
if val:
val.thisown = True
return val
def set_name(self, name):
"""Set /Name (icon) of annotation."""
CheckParent(self)
return _fitz_old.Annot_set_name(self, name)
def set_rect(self, rect):
"""Set annotation rectangle."""
CheckParent(self)
return _fitz_old.Annot_set_rect(self, rect)
def set_rotation(self, rotate=0):
"""Set annotation rotation."""
CheckParent(self)
return _fitz_old.Annot_set_rotation(self, rotate)
@property
def rotation(self):
"""annotation rotation"""
CheckParent(self)
return _fitz_old.Annot_rotation(self)
@property
def vertices(self):
"""annotation vertex points"""
CheckParent(self)
return _fitz_old.Annot_vertices(self)
@property
def colors(self):
"""Color definitions."""
CheckParent(self)
return _fitz_old.Annot_colors(self)
def _update_appearance(self, opacity=-1, blend_mode=None, fill_color=None, rotate=-1):
return _fitz_old.Annot__update_appearance(self, opacity, blend_mode, fill_color, rotate)
def update(self,
blend_mode: OptStr =None,
opacity: OptFloat =None,
fontsize: float =0,
fontname: OptStr =None,
text_color: OptSeq =None,
border_color: OptSeq =None,
fill_color: OptSeq =None,
cross_out: bool =True,
rotate: int =-1,
):
"""Update annot appearance.
Notes:
Depending on the annot type, some parameters make no sense,
while others are only available in this method to achieve the
desired result. This is especially true for 'FreeText' annots.
Args:
blend_mode: set the blend mode, all annotations.
opacity: set the opacity, all annotations.
fontsize: set fontsize, 'FreeText' only.
fontname: set the font, 'FreeText' only.
border_color: set border color, 'FreeText' only.
text_color: set text color, 'FreeText' only.
fill_color: set fill color, all annotations.
cross_out: draw diagonal lines, 'Redact' only.
rotate: set rotation, 'FreeText' and some others.
"""
CheckParent(self)
def color_string(cs, code):
"""Return valid PDF color operator for a given color sequence.
"""
cc = ColorCode(cs, code)
if not cc:
return b""
return (cc + "\n").encode()
annot_type = self.type[0] # get the annot type
dt = self.border.get("dashes", None) # get the dashes spec
bwidth = self.border.get("width", -1) # get border line width
stroke = self.colors["stroke"] # get the stroke color
if fill_color != None: # change of fill color requested
fill = fill_color
else: # put in current annot value
fill = self.colors["fill"]
rect = None # self.rect # prevent MuPDF fiddling with it
apnmat = self.apn_matrix # prevent MuPDF fiddling with it
if rotate != -1: # sanitize rotation value
while rotate < 0:
rotate += 360
while rotate >= 360:
rotate -= 360
if annot_type == PDF_ANNOT_FREE_TEXT and rotate % 90 != 0:
rotate = 0
#------------------------------------------------------------------
# handle opacity and blend mode
#------------------------------------------------------------------
if blend_mode is None:
blend_mode = self.blendmode
if not hasattr(opacity, "__float__"):
opacity = self.opacity
if 0 <= opacity < 1 or blend_mode is not None:
opa_code = "/H gs\n" # then we must reference this 'gs'
else:
opa_code = ""
if annot_type == PDF_ANNOT_FREE_TEXT:
CheckColor(border_color)
CheckColor(text_color)
CheckColor(fill_color)
tcol, fname, fsize = TOOLS._parse_da(self)
# read and update default appearance as necessary
update_default_appearance = False
if fsize <= 0:
fsize = 12
update_default_appearance = True
if text_color is not None:
tcol = text_color
update_default_appearance = True
if fontname is not None:
fname = fontname
update_default_appearance = True
if fontsize > 0:
fsize = fontsize
update_default_appearance = True
if update_default_appearance:
da_str = ""
if len(tcol) == 3:
fmt = "{:g} {:g} {:g} rg /{f:s} {s:g} Tf"
elif len(tcol) == 1:
fmt = "{:g} g /{f:s} {s:g} Tf"
elif len(tcol) == 4:
fmt = "{:g} {:g} {:g} {:g} k /{f:s} {s:g} Tf"
da_str = fmt.format(*tcol, f=fname, s=fsize)
TOOLS._update_da(self, da_str)
#------------------------------------------------------------------
# now invoke MuPDF to update the annot appearance
#------------------------------------------------------------------
val = self._update_appearance(
opacity=opacity,
blend_mode=blend_mode,
fill_color=fill,
rotate=rotate,
)
if val == False:
raise RuntimeError("Error updating annotation.")
bfill = color_string(fill, "f")
bstroke = color_string(stroke, "c")
p_ctm = self.parent.transformation_matrix
imat = ~p_ctm # inverse page transf. matrix
if dt:
dashes = "[" + " ".join(map(str, dt)) + "] 0 d\n"
dashes = dashes.encode("utf-8")
else:
dashes = None
if self.line_ends:
line_end_le, line_end_ri = self.line_ends
else:
line_end_le, line_end_ri = 0, 0 # init line end codes
# read contents as created by MuPDF
ap = self._getAP()
ap_tab = ap.splitlines() # split in single lines
ap_updated = False # assume we did nothing
if annot_type == PDF_ANNOT_REDACT:
if cross_out: # create crossed-out rect
ap_updated = True
ap_tab = ap_tab[:-1]
_, LL, LR, UR, UL = ap_tab
ap_tab.append(LR)
ap_tab.append(LL)
ap_tab.append(UR)
ap_tab.append(LL)
ap_tab.append(UL)
ap_tab.append(b"S")
if bwidth > 0 or bstroke != b"":
ap_updated = True
ntab = [b"%g w" % bwidth] if bwidth > 0 else []
for line in ap_tab:
if line.endswith(b"w"):
continue
if line.endswith(b"RG") and bstroke != b"":
line = bstroke[:-1]
ntab.append(line)
ap_tab = ntab
ap = b"\n".join(ap_tab)
if annot_type == PDF_ANNOT_FREE_TEXT:
BT = ap.find(b"BT")
ET = ap.find(b"ET") + 2
ap = ap[BT:ET]
w, h = self.rect.width, self.rect.height
if rotate in (90, 270) or not (apnmat.b == apnmat.c == 0):
w, h = h, w
re = b"0 0 %g %g re" % (w, h)
ap = re + b"\nW\nn\n" + ap
ope = None
fill_string = color_string(fill, "f")
if fill_string:
ope = b"f"
stroke_string = color_string(border_color, "c")
if stroke_string and bwidth > 0:
ope = b"S"
bwidth = b"%g w\n" % bwidth
else:
bwidth = stroke_string = b""
if fill_string and stroke_string:
ope = b"B"
if ope != None:
ap = bwidth + fill_string + stroke_string + re + b"\n" + ope + b"\n" + ap
if dashes != None: # handle dashes
ap = dashes + b"\n" + ap
dashes = None
ap_updated = True
if annot_type in (PDF_ANNOT_POLYGON, PDF_ANNOT_POLY_LINE):
ap = b"\n".join(ap_tab[:-1]) + b"\n"
ap_updated = True
if bfill != b"":
if annot_type == PDF_ANNOT_POLYGON:
ap = ap + bfill + b"b" # close, fill, and stroke
elif annot_type == PDF_ANNOT_POLY_LINE:
ap = ap + b"S" # stroke
else:
if annot_type == PDF_ANNOT_POLYGON:
ap = ap + b"s" # close and stroke
elif annot_type == PDF_ANNOT_POLY_LINE:
ap = ap + b"S" # stroke
if dashes is not None: # handle dashes
ap = dashes + ap
# reset dashing - only applies for LINE annots with line ends given
ap = ap.replace(b"\nS\n", b"\nS\n[] 0 d\n", 1)
ap_updated = True
if opa_code:
ap = opa_code.encode("utf-8") + ap
ap_updated = True
ap = b"q\n" + ap + b"\nQ\n"
#----------------------------------------------------------------------
# the following handles line end symbols for 'Polygon' and 'Polyline'
#----------------------------------------------------------------------
if line_end_le + line_end_ri > 0 and annot_type in (PDF_ANNOT_POLYGON, PDF_ANNOT_POLY_LINE):
le_funcs = (None, TOOLS._le_square, TOOLS._le_circle,
TOOLS._le_diamond, TOOLS._le_openarrow,
TOOLS._le_closedarrow, TOOLS._le_butt,
TOOLS._le_ropenarrow, TOOLS._le_rclosedarrow,
TOOLS._le_slash)
le_funcs_range = range(1, len(le_funcs))
d = 2 * max(1, self.border["width"])
rect = self.rect + (-d, -d, d, d)
ap_updated = True
points = self.vertices
if line_end_le in le_funcs_range:
p1 = Point(points[0]) * imat
p2 = Point(points[1]) * imat
left = le_funcs[line_end_le](self, p1, p2, False, fill_color)
ap += left.encode()
if line_end_ri in le_funcs_range:
p1 = Point(points[-2]) * imat
p2 = Point(points[-1]) * imat
left = le_funcs[line_end_ri](self, p1, p2, True, fill_color)
ap += left.encode()
if ap_updated:
if rect: # rect modified here?
self.set_rect(rect)
self._setAP(ap, rect=1)
else:
self._setAP(ap, rect=0)
#-------------------------------
# handle annotation rotations
#-------------------------------
if annot_type not in ( # only these types are supported
PDF_ANNOT_CARET,
PDF_ANNOT_CIRCLE,
PDF_ANNOT_FILE_ATTACHMENT,
PDF_ANNOT_INK,
PDF_ANNOT_LINE,
PDF_ANNOT_POLY_LINE,
PDF_ANNOT_POLYGON,
PDF_ANNOT_SQUARE,
PDF_ANNOT_STAMP,
PDF_ANNOT_TEXT,
):
return
rot = self.rotation # get value from annot object
if rot == -1: # nothing to change
return
M = (self.rect.tl + self.rect.br) / 2 # center of annot rect
if rot == 0: # undo rotations
if abs(apnmat - Matrix(1, 1)) < 1e-5:
return # matrix already is a no-op
quad = self.rect.morph(M, ~apnmat) # derotate rect
self.set_rect(quad.rect)
self.set_apn_matrix(Matrix(1, 1)) # appearance matrix = no-op
return
mat = Matrix(rot)
quad = self.rect.morph(M, mat)
self.set_rect(quad.rect)
self.set_apn_matrix(apnmat * mat)
def set_colors(self, colors=None, stroke=None, fill=None):
"""Set 'stroke' and 'fill' colors.
Use either a dict or the direct arguments.
"""
CheckParent(self)
doc = self.parent.parent
if type(colors) is not dict:
colors = {"fill": fill, "stroke": stroke}
fill = colors.get("fill")
stroke = colors.get("stroke")
fill_annots = (PDF_ANNOT_CIRCLE, PDF_ANNOT_SQUARE, PDF_ANNOT_LINE, PDF_ANNOT_POLY_LINE, PDF_ANNOT_POLYGON,
PDF_ANNOT_REDACT,)
if stroke in ([], ()):
doc.xref_set_key(self.xref, "C", "[]")
elif stroke is not None:
if hasattr(stroke, "__float__"):
stroke = [float(stroke)]
CheckColor(stroke)
if len(stroke) == 1:
s = "[%g]" % stroke[0]
elif len(stroke) == 3:
s = "[%g %g %g]" % tuple(stroke)
else:
s = "[%g %g %g %g]" % tuple(stroke)
doc.xref_set_key(self.xref, "C", s)
if fill and self.type[0] not in fill_annots:
print("Warning: fill color ignored for annot type '%s'." % self.type[1])
return
if fill in ([], ()):
doc.xref_set_key(self.xref, "IC", "[]")
elif fill is not None:
if hasattr(fill, "__float__"):
fill = [float(fill)]
CheckColor(fill)
if len(fill) == 1:
s = "[%g]" % fill[0]
elif len(fill) == 3:
s = "[%g %g %g]" % tuple(fill)
else:
s = "[%g %g %g %g]" % tuple(fill)
doc.xref_set_key(self.xref, "IC", s)
@property
def line_ends(self):
"""Line end codes."""
CheckParent(self)
return _fitz_old.Annot_line_ends(self)
def set_line_ends(self, start, end):
"""Set line end codes."""
CheckParent(self)
return _fitz_old.Annot_set_line_ends(self, start, end)
@property
def type(self):
"""annotation type"""
CheckParent(self)
return _fitz_old.Annot_type(self)
@property
def opacity(self):
"""Opacity."""
CheckParent(self)
return _fitz_old.Annot_opacity(self)
def set_opacity(self, opacity):
"""Set opacity."""
CheckParent(self)
return _fitz_old.Annot_set_opacity(self, opacity)
@property
def file_info(self):
"""Attached file information."""
CheckParent(self)
return _fitz_old.Annot_file_info(self)
def get_file(self):
"""Retrieve attached file content."""
CheckParent(self)
return _fitz_old.Annot_get_file(self)
def get_sound(self):
"""Retrieve sound stream."""
CheckParent(self)
return _fitz_old.Annot_get_sound(self)
def update_file(self, buffer=None, filename=None, ufilename=None, desc=None):
"""Update attached file."""
CheckParent(self)
return _fitz_old.Annot_update_file(self, buffer, filename, ufilename, desc)
@property
def info(self):
"""Various information details."""
CheckParent(self)
return _fitz_old.Annot_info(self)
def set_info(self, info=None, content=None, title=None, creationDate=None, modDate=None, subject=None):
"""Set various properties."""
CheckParent(self)
if type(info) is dict: # build the args from the dictionary
content = info.get("content", None)
title = info.get("title", None)
creationDate = info.get("creationDate", None)
modDate = info.get("modDate", None)
subject = info.get("subject", None)
info = None
return _fitz_old.Annot_set_info(self, info, content, title, creationDate, modDate, subject)
@property
def border(self):
"""Border information."""
CheckParent(self)
atype = self.type[0]
if atype not in (PDF_ANNOT_CIRCLE, PDF_ANNOT_FREE_TEXT, PDF_ANNOT_INK, PDF_ANNOT_LINE, PDF_ANNOT_POLY_LINE,PDF_ANNOT_POLYGON, PDF_ANNOT_SQUARE):
return {}
return _fitz_old.Annot_border(self)
def set_border(self, border=None, width=-1, style=None, dashes=None, clouds=-1):
"""Set border properties.
Either a dict, or direct arguments width, style, dashes or clouds."""
CheckParent(self)
atype, atname = self.type[:2] # annotation type
if atype not in (PDF_ANNOT_CIRCLE, PDF_ANNOT_FREE_TEXT, PDF_ANNOT_INK, PDF_ANNOT_LINE, PDF_ANNOT_POLY_LINE,PDF_ANNOT_POLYGON, PDF_ANNOT_SQUARE):
print(f"Cannot set border for '{atname}'.")
return None
if not atype in (PDF_ANNOT_CIRCLE, PDF_ANNOT_FREE_TEXT,PDF_ANNOT_POLYGON, PDF_ANNOT_SQUARE):
if clouds > 0:
print(f"Cannot set cloudy border for '{atname}'.")
clouds = -1 # do not set border effect
if type(border) is not dict:
border = {"width": width, "style": style, "dashes": dashes, "clouds": clouds}
border.setdefault("width", -1)
border.setdefault("style", None)
border.setdefault("dashes", None)
border.setdefault("clouds", -1)
if border["width"] == None:
border["width"] = -1
if border["clouds"] == None:
border["clouds"] = -1
if hasattr(border["dashes"], "__getitem__"): # ensure sequence items are integers
border["dashes"] = tuple(border["dashes"])
for item in border["dashes"]:
if not isinstance(item, int):
border["dashes"] = None
break
return _fitz_old.Annot_set_border(self, border, width, style, dashes, clouds)
@property
def flags(self):
"""Flags field."""
CheckParent(self)
return _fitz_old.Annot_flags(self)
def clean_contents(self, sanitize=1):
"""Clean appearance contents stream."""
CheckParent(self)
return _fitz_old.Annot_clean_contents(self, sanitize)
def set_flags(self, flags):
"""Set annotation flags."""
CheckParent(self)
return _fitz_old.Annot_set_flags(self, flags)
def delete_responses(self):
"""Delete 'Popup' and responding annotations."""
CheckParent(self)
return _fitz_old.Annot_delete_responses(self)
@property
def next(self):
"""Next annotation."""
CheckParent(self)
val = _fitz_old.Annot_next(self)
if not val:
return None
val.thisown = True
val.parent = self.parent # copy owning page object from previous annot
val.parent._annot_refs[id(val)] = val
if val.type[0] == PDF_ANNOT_WIDGET:
widget = Widget()
TOOLS._fill_widget(val, widget)
val = widget
return val
def get_pixmap(self, matrix=None, dpi=None, colorspace=None, alpha=0):
"""annotation Pixmap"""
CheckParent(self)
cspaces = {"gray": csGRAY, "rgb": csRGB, "cmyk": csCMYK}
if type(colorspace) is str:
colorspace = cspaces.get(colorspace.lower(), None)
if dpi:
matrix = Matrix(dpi / 72, dpi / 72)
val = _fitz_old.Annot_get_pixmap(self, matrix, dpi, colorspace, alpha)
val.thisown = True
if dpi:
val.set_dpi(dpi, dpi)
return val
def _erase(self):
self.__swig_destroy__(self)
self.parent = None
def __str__(self):
CheckParent(self)
return "'%s' annotation on %s" % (self.type[1], str(self.parent))
def __repr__(self):
CheckParent(self)
return "'%s' annotation on %s" % (self.type[1], str(self.parent))
def __del__(self):
if self.parent is None:
return
self._erase()
# Register Annot in _fitz_old:
_fitz_old.Annot_swigregister(Annot)
class Link(object):
thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag")
def __init__(self, *args, **kwargs):
raise AttributeError("No constructor defined")
__repr__ = _swig_repr
__swig_destroy__ = _fitz_old.delete_Link
def _border(self, doc, xref):
return _fitz_old.Link__border(self, doc, xref)
def _setBorder(self, border, doc, xref):
return _fitz_old.Link__setBorder(self, border, doc, xref)
def _colors(self, doc, xref):
return _fitz_old.Link__colors(self, doc, xref)
@property
def border(self):
return self._border(self.parent.parent.this, self.xref)
@property
def flags(self)->int:
CheckParent(self)
doc = self.parent.parent
if not doc.is_pdf:
return 0
f = doc.xref_get_key(self.xref, "F")
if f[1] != "null":
return int(f[1])
return 0
def set_flags(self, flags):
CheckParent(self)
doc = self.parent.parent
if not doc.is_pdf:
raise ValueError("is no PDF")
if not type(flags) is int:
raise ValueError("bad 'flags' value")
doc.xref_set_key(self.xref, "F", str(flags))
return None
def set_border(self, border=None, width=0, dashes=None, style=None):
if type(border) is not dict:
border = {"width": width, "style": style, "dashes": dashes}
return self._setBorder(border, self.parent.parent.this, self.xref)
@property
def colors(self):
return self._colors(self.parent.parent.this, self.xref)
def set_colors(self, colors=None, stroke=None, fill=None):
"""Set border colors."""
CheckParent(self)
doc = self.parent.parent
if type(colors) is not dict:
colors = {"fill": fill, "stroke": stroke}
fill = colors.get("fill")
stroke = colors.get("stroke")
if fill is not None:
print("warning: links have no fill color")
if stroke in ([], ()):
doc.xref_set_key(self.xref, "C", "[]")
return
if hasattr(stroke, "__float__"):
stroke = [float(stroke)]
CheckColor(stroke)
if len(stroke) == 1:
s = "[%g]" % stroke[0]
elif len(stroke) == 3:
s = "[%g %g %g]" % tuple(stroke)
else:
s = "[%g %g %g %g]" % tuple(stroke)
doc.xref_set_key(self.xref, "C", s)
@property
def uri(self):
"""Uri string."""
CheckParent(self)
return _fitz_old.Link_uri(self)
@property
def is_external(self):
"""Flag the link as external."""
CheckParent(self)
return _fitz_old.Link_is_external(self)
page = -1
@property
def dest(self):
"""Create link destination details."""
if hasattr(self, "parent") and self.parent is None:
raise ValueError("orphaned object: parent is None")
if self.parent.parent.is_closed or self.parent.parent.is_encrypted:
raise ValueError("document closed or encrypted")
doc = self.parent.parent
if self.is_external or self.uri.startswith("#"):
uri = None
else:
uri = doc.resolve_link(self.uri)
return linkDest(self, uri)
@property
def rect(self):
"""Rectangle ('hot area')."""
CheckParent(self)
val = _fitz_old.Link_rect(self)
val = Rect(val)
return val
@property
def next(self):
"""Next link."""
CheckParent(self)
val = _fitz_old.Link_next(self)
if val:
val.thisown = True
val.parent = self.parent # copy owning page from prev link
val.parent._annot_refs[id(val)] = val
if self.xref > 0: # prev link has an xref
link_xrefs = [x[0] for x in self.parent.annot_xrefs() if x[1] == PDF_ANNOT_LINK]
link_ids = [x[2] for x in self.parent.annot_xrefs() if x[1] == PDF_ANNOT_LINK]
idx = link_xrefs.index(self.xref)
val.xref = link_xrefs[idx + 1]
val.id = link_ids[idx + 1]
else:
val.xref = 0
val.id = ""
return val
def _erase(self):
self.__swig_destroy__(self)
self.parent = None
def __str__(self):
CheckParent(self)
return "link on " + str(self.parent)
def __repr__(self):
CheckParent(self)
return "link on " + str(self.parent)
def __del__(self):
self._erase()
# Register Link in _fitz_old:
_fitz_old.Link_swigregister(Link)
class DisplayList(object):
thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag")
__repr__ = _swig_repr
__swig_destroy__ = _fitz_old.delete_DisplayList
def __init__(self, mediabox):
_fitz_old.DisplayList_swiginit(self, _fitz_old.new_DisplayList(mediabox))
def run(self, dw, m, area):
return _fitz_old.DisplayList_run(self, dw, m, area)
@property
def rect(self):
val = _fitz_old.DisplayList_rect(self)
val = Rect(val)
return val
def get_pixmap(self, matrix=None, colorspace=None, alpha=0, clip=None):
val = _fitz_old.DisplayList_get_pixmap(self, matrix, colorspace, alpha, clip)
val.thisown = True
return val
def get_textpage(self, flags=3):
val = _fitz_old.DisplayList_get_textpage(self, flags)
val.thisown = True
return val
def __del__(self):
if not type(self) is DisplayList:
return
if getattr(self, "thisown", False):
self.__swig_destroy__(self)
# Register DisplayList in _fitz_old:
_fitz_old.DisplayList_swigregister(DisplayList)
class TextPage(object):
thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag")
__repr__ = _swig_repr
__swig_destroy__ = _fitz_old.delete_TextPage
def __init__(self, mediabox):
_fitz_old.TextPage_swiginit(self, _fitz_old.new_TextPage(mediabox))
self.thisown=True
def search(self, needle, hit_max=0, quads=1):
"""Locate 'needle' returning rects or quads."""
val = _fitz_old.TextPage_search(self, needle, hit_max, quads)
if not val:
return val
items = len(val)
for i in range(items): # change entries to quads or rects
q = Quad(val[i])
if quads:
val[i] = q
else:
val[i] = q.rect
if quads:
return val
i = 0 # join overlapping rects on the same line
while i < items - 1:
v1 = val[i]
v2 = val[i + 1]
if v1.y1 != v2.y1 or (v1 & v2).is_empty:
i += 1
continue # no overlap on same line
val[i] = v1 | v2 # join rectangles
del val[i + 1] # remove v2
items -= 1 # reduce item count
return val
def _getNewBlockList(self, page_dict, raw):
return _fitz_old.TextPage__getNewBlockList(self, page_dict, raw)
def _textpage_dict(self, raw=False):
page_dict = {"width": self.rect.width, "height": self.rect.height}
self._getNewBlockList(page_dict, raw)
return page_dict
def extractIMGINFO(self, hashes=0):
"""Return a list with image meta information."""
return _fitz_old.TextPage_extractIMGINFO(self, hashes)
def extractBLOCKS(self):
"""Return a list with text block information."""
return _fitz_old.TextPage_extractBLOCKS(self)
def extractWORDS(self, delimiters=None):
"""Return a list with text word information."""
return _fitz_old.TextPage_extractWORDS(self, delimiters)
def poolsize(self):
"""TextPage current poolsize."""
return _fitz_old.TextPage_poolsize(self)
@property
def rect(self):
"""TextPage rectangle."""
val = _fitz_old.TextPage_rect(self)
val = Rect(val)
return val
def _extractText(self, format):
return _fitz_old.TextPage__extractText(self, format)
def extractTextbox(self, rect):
return _fitz_old.TextPage_extractTextbox(self, rect)
def extractSelection(self, pointa, pointb):
return _fitz_old.TextPage_extractSelection(self, pointa, pointb)
def extractText(self, sort=False) -> str:
"""Return simple, bare text on the page."""
if sort is False:
return self._extractText(0)
blocks = self.extractBLOCKS()[:]
blocks.sort(key=lambda b: (b[3], b[0]))
return "".join([b[4] for b in blocks])
def extractHTML(self) -> str:
"""Return page content as a HTML string."""
return self._extractText(1)
def extractJSON(self, cb=None, sort=False) -> str:
"""Return 'extractDICT' converted to JSON format."""
import base64, json
val = self._textpage_dict(raw=False)
class b64encode(json.JSONEncoder):
def default(self, s):
if type(s) in (bytes, bytearray):
return base64.b64encode(s).decode()
if cb is not None:
val["width"] = cb.width
val["height"] = cb.height
if sort is True:
blocks = val["blocks"]
blocks.sort(key=lambda b: (b["bbox"][3], b["bbox"][0]))
val["blocks"] = blocks
val = json.dumps(val, separators=(",", ":"), cls=b64encode, indent=1)
return val
def extractRAWJSON(self, cb=None, sort=False) -> str:
"""Return 'extractRAWDICT' converted to JSON format."""
import base64, json
val = self._textpage_dict(raw=True)
class b64encode(json.JSONEncoder):
def default(self,s):
if type(s) in (bytes, bytearray):
return base64.b64encode(s).decode()
if cb is not None:
val["width"] = cb.width
val["height"] = cb.height
if sort is True:
blocks = val["blocks"]
blocks.sort(key=lambda b: (b["bbox"][3], b["bbox"][0]))
val["blocks"] = blocks
val = json.dumps(val, separators=(",", ":"), cls=b64encode, indent=1)
return val
def extractXML(self) -> str:
"""Return page content as a XML string."""
return self._extractText(3)
def extractXHTML(self) -> str:
"""Return page content as a XHTML string."""
return self._extractText(4)
def extractDICT(self, cb=None, sort=False) -> dict:
"""Return page content as a Python dict of images and text spans."""
val = self._textpage_dict(raw=False)
if cb is not None:
val["width"] = cb.width
val["height"] = cb.height
if sort is True:
blocks = val["blocks"]
blocks.sort(key=lambda b: (b["bbox"][3], b["bbox"][0]))
val["blocks"] = blocks
return val
def extractRAWDICT(self, cb=None, sort=False) -> dict:
"""Return page content as a Python dict of images and text characters."""
val = self._textpage_dict(raw=True)
if cb is not None:
val["width"] = cb.width
val["height"] = cb.height
if sort is True:
blocks = val["blocks"]
blocks.sort(key=lambda b: (b["bbox"][3], b["bbox"][0]))
val["blocks"] = blocks
return val
def __del__(self):
if not type(self) is TextPage:
return
if getattr(self, "thisown", False):
self.__swig_destroy__(self)
# Register TextPage in _fitz_old:
_fitz_old.TextPage_swigregister(TextPage)
class Graftmap(object):
thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag")
__repr__ = _swig_repr
__swig_destroy__ = _fitz_old.delete_Graftmap
def __init__(self, doc):
_fitz_old.Graftmap_swiginit(self, _fitz_old.new_Graftmap(doc))
def __del__(self):
if not type(self) is Graftmap:
return
if getattr(self, "thisown", False):
self.__swig_destroy__(self)
# Register Graftmap in _fitz_old:
_fitz_old.Graftmap_swigregister(Graftmap)
class TextWriter(object):
thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag")
__repr__ = _swig_repr
__swig_destroy__ = _fitz_old.delete_TextWriter
def __init__(self, page_rect, opacity=1, color=None):
"""Stores text spans for later output on compatible PDF pages."""
_fitz_old.TextWriter_swiginit(self, _fitz_old.new_TextWriter(page_rect, opacity, color))
self.opacity = opacity
self.color = color
self.rect = Rect(page_rect)
self.ctm = Matrix(1, 0, 0, -1, 0, self.rect.height)
self.ictm = ~self.ctm
self.last_point = Point()
self.last_point.__doc__ = "Position following last text insertion."
self.text_rect = Rect()
self.text_rect.__doc__ = "Accumulated area of text spans."
self.used_fonts = set()
self.thisown = True
def append(self, pos, text, font=None, fontsize=11, language=None, right_to_left=0, small_caps=0):
"""Store 'text' at point 'pos' using 'font' and 'fontsize'."""
pos = Point(pos) * self.ictm
if font is None:
font = Font("helv")
if not font.is_writable:
raise ValueError("Unsupported font '%s'." % font.name)
if right_to_left:
text = self.clean_rtl(text)
text = "".join(reversed(text))
right_to_left = 0
val = _fitz_old.TextWriter_append(self, pos, text, font, fontsize, language, right_to_left, small_caps)
self.last_point = Point(val[-2:]) * self.ctm
self.text_rect = self._bbox * self.ctm
val = self.text_rect, self.last_point
if font.flags["mono"] == 1:
self.used_fonts.add(font)
return val
def appendv(self, pos, text, font=None, fontsize=11,
language=None, small_caps=False):
"""Append text in vertical write mode."""
lheight = fontsize * 1.2
for c in text:
self.append(pos, c, font=font, fontsize=fontsize,
language=language, small_caps=small_caps)
pos.y += lheight
return self.text_rect, self.last_point
def clean_rtl(self, text):
"""Revert the sequence of Latin text parts.
Text with right-to-left writing direction (Arabic, Hebrew) often
contains Latin parts, which are written in left-to-right: numbers, names,
etc. For output as PDF text we need *everything* in right-to-left.
E.g. an input like "<arabic> ABCDE FG HIJ <arabic> KL <arabic>" will be
converted to "<arabic> JIH GF EDCBA <arabic> LK <arabic>". The Arabic
parts remain untouched.
Args:
text: str
Returns:
Massaged string.
"""
if not text:
return text
# split into words at space boundaries
words = text.split(" ")
idx = []
for i in range(len(words)):
w = words[i]
# revert character sequence for Latin only words
if not (len(w) < 2 or max([ord(c) for c in w]) > 255):
words[i] = "".join(reversed(w))
idx.append(i) # stored index of Latin word
# adjacent Latin words must revert their sequence, too
idx2 = [] # store indices of adjacent Latin words
for i in range(len(idx)):
if idx2 == []: # empty yet?
idx2.append(idx[i]) # store Latin word number
elif idx[i] > idx2[-1] + 1: # large gap to last?
if len(idx2) > 1: # at least two consecutives?
words[idx2[0] : idx2[-1] + 1] = reversed(
words[idx2[0] : idx2[-1] + 1]
) # revert their sequence
idx2 = [idx[i]] # re-initialize
elif idx[i] == idx2[-1] + 1: # new adjacent Latin word
idx2.append(idx[i])
text = " ".join(words)
return text
@property
def _bbox(self):
val = _fitz_old.TextWriter__bbox(self)
val = Rect(val)
return val
def write_text(self, page, color=None, opacity=-1, overlay=1, morph=None, matrix=None, render_mode=0, oc=0):
"""Write the text to a PDF page having the TextWriter's page size.
Args:
page: a PDF page having same size.
color: override text color.
opacity: override transparency.
overlay: put in foreground or background.
morph: tuple(Point, Matrix), apply a matrix with a fixpoint.
matrix: Matrix to be used instead of 'morph' argument.
render_mode: (int) PDF render mode operator 'Tr'.
"""
CheckParent(page)
if abs(self.rect - page.rect) > 1e-3:
raise ValueError("incompatible page rect")
if morph != None:
if (type(morph) not in (tuple, list)
or type(morph[0]) is not Point
or type(morph[1]) is not Matrix
):
raise ValueError("morph must be (Point, Matrix) or None")
if matrix != None and morph != None:
raise ValueError("only one of matrix, morph is allowed")
if getattr(opacity, "__float__", None) is None or opacity == -1:
opacity = self.opacity
if color is None:
color = self.color
val = _fitz_old.TextWriter_write_text(self, page, color, opacity, overlay, morph, matrix, render_mode, oc)
max_nums = val[0]
content = val[1]
max_alp, max_font = max_nums
old_cont_lines = content.splitlines()
optcont = page._get_optional_content(oc)
if optcont != None:
bdc = "/OC /%s BDC" % optcont
emc = "EMC"
else:
bdc = emc = ""
new_cont_lines = ["q"]
if bdc:
new_cont_lines.append(bdc)
cb = page.cropbox_position
if page.rotation in (90, 270):
delta = page.rect.height - page.rect.width
else:
delta = 0
mb = page.mediabox
if bool(cb) or mb.y0 != 0 or delta != 0:
new_cont_lines.append("1 0 0 1 %g %g cm" % (cb.x, cb.y + mb.y0 - delta))
if morph:
p = morph[0] * self.ictm
delta = Matrix(1, 1).pretranslate(p.x, p.y)
matrix = ~delta * morph[1] * delta
if morph or matrix:
new_cont_lines.append("%g %g %g %g %g %g cm" % JM_TUPLE(matrix))
for line in old_cont_lines:
if line.endswith(" cm"):
continue
if line == "BT":
new_cont_lines.append(line)
new_cont_lines.append("%i Tr" % render_mode)
continue
if line.endswith(" gs"):
alp = int(line.split()[0][4:]) + max_alp
line = "/Alp%i gs" % alp
elif line.endswith(" Tf"):
temp = line.split()
fsize = float(temp[1])
if render_mode != 0:
w = fsize * 0.05
else:
w = 1
new_cont_lines.append("%g w" % w)
font = int(temp[0][2:]) + max_font
line = " ".join(["/F%i" % font] + temp[1:])
elif line.endswith(" rg"):
new_cont_lines.append(line.replace("rg", "RG"))
elif line.endswith(" g"):
new_cont_lines.append(line.replace(" g", " G"))
elif line.endswith(" k"):
new_cont_lines.append(line.replace(" k", " K"))
new_cont_lines.append(line)
if emc:
new_cont_lines.append(emc)
new_cont_lines.append("Q\n")
content = "\n".join(new_cont_lines).encode("utf-8")
TOOLS._insert_contents(page, content, overlay=overlay)
val = None
for font in self.used_fonts:
repair_mono_font(page, font)
return val
def __del__(self):
if not type(self) is TextWriter:
return
if getattr(self, "thisown", False):
self.__swig_destroy__(self)
# Register TextWriter in _fitz_old:
_fitz_old.TextWriter_swigregister(TextWriter)
class Font(object):
thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag")
__repr__ = _swig_repr
__swig_destroy__ = _fitz_old.delete_Font
def __init__(self, fontname=None, fontfile=None, fontbuffer=None, script=0, language=None, ordering=-1, is_bold=0, is_italic=0, is_serif=0, embed=1):
if fontbuffer:
if hasattr(fontbuffer, "getvalue"):
fontbuffer = fontbuffer.getvalue()
elif isinstance(fontbuffer, bytearray):
fontbuffer = bytes(fontbuffer)
if not isinstance(fontbuffer, bytes):
raise ValueError("bad type: 'fontbuffer'")
if isinstance(fontname, str):
fname_lower = fontname.lower()
if "/" in fname_lower or "\\" in fname_lower or "." in fname_lower:
print("Warning: did you mean a fontfile?")
if fname_lower in ("cjk", "china-t", "china-ts"):
ordering = 0
elif fname_lower.startswith("china-s"):
ordering = 1
elif fname_lower.startswith("korea"):
ordering = 3
elif fname_lower.startswith("japan"):
ordering = 2
elif fname_lower in fitz_fontdescriptors.keys():
import pymupdf_fonts # optional fonts
fontbuffer = pymupdf_fonts.myfont(fname_lower) # make a copy
fontname = None # ensure using fontbuffer only
del pymupdf_fonts # remove package again
elif ordering < 0:
fontname = Base14_fontdict.get(fontname, fontname)
_fitz_old.Font_swiginit(self, _fitz_old.new_Font(fontname, fontfile, fontbuffer, script, language, ordering, is_bold, is_italic, is_serif, embed))
self.thisown = True
def glyph_advance(self, chr, language=None, script=0, wmode=0, small_caps=0):
"""Return the glyph width of a unicode (font size 1)."""
return _fitz_old.Font_glyph_advance(self, chr, language, script, wmode, small_caps)
def text_length(self, text, fontsize=11, language=None, script=0, wmode=0, small_caps=0):
"""Return length of unicode 'text' under a fontsize."""
return _fitz_old.Font_text_length(self, text, fontsize, language, script, wmode, small_caps)
def char_lengths(self, text, fontsize=11, language=None, script=0, wmode=0, small_caps=0):
"""Return tuple of char lengths of unicode 'text' under a fontsize."""
return _fitz_old.Font_char_lengths(self, text, fontsize, language, script, wmode, small_caps)
def glyph_bbox(self, chr, language=None, script=0, small_caps=0):
"""Return the glyph bbox of a unicode (font size 1)."""
val = _fitz_old.Font_glyph_bbox(self, chr, language, script, small_caps)
val = Rect(val)
return val
def has_glyph(self, chr, language=None, script=0, fallback=0, small_caps=0):
"""Check whether font has a glyph for this unicode."""
return _fitz_old.Font_has_glyph(self, chr, language, script, fallback, small_caps)
def valid_codepoints(self):
from array import array
gc = self.glyph_count
cp = array("l", (0,) * gc)
arr = cp.buffer_info()
self._valid_unicodes(arr)
return array("l", sorted(set(cp))[1:])
def _valid_unicodes(self, arr):
return _fitz_old.Font__valid_unicodes(self, arr)
@property
def flags(self):
return _fitz_old.Font_flags(self)
@property
def is_bold(self):
return _fitz_old.Font_is_bold(self)
@property
def is_serif(self):
return _fitz_old.Font_is_serif(self)
@property
def is_italic(self):
return _fitz_old.Font_is_italic(self)
@property
def is_monospaced(self):
return _fitz_old.Font_is_monospaced(self)
@property
def name(self):
return _fitz_old.Font_name(self)
@property
def glyph_count(self):
return _fitz_old.Font_glyph_count(self)
@property
def buffer(self):
return _fitz_old.Font_buffer(self)
@property
def bbox(self):
val = _fitz_old.Font_bbox(self)
val = Rect(val)
return val
@property
def ascender(self):
"""Return the glyph ascender value."""
return _fitz_old.Font_ascender(self)
@property
def descender(self):
"""Return the glyph descender value."""
return _fitz_old.Font_descender(self)
@property
def is_writable(self):
return True
def glyph_name_to_unicode(self, name):
"""Return the unicode for a glyph name."""
return glyph_name_to_unicode(name)
def unicode_to_glyph_name(self, ch):
"""Return the glyph name for a unicode."""
return unicode_to_glyph_name(ch)
def __repr__(self):
return "Font('%s')" % self.name
def __del__(self):
if not type(self) is Font:
return
if getattr(self, "thisown", False):
self.__swig_destroy__(self)
# Register Font in _fitz_old:
_fitz_old.Font_swigregister(Font)
class DocumentWriter(object):
thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag")
__repr__ = _swig_repr
__swig_destroy__ = _fitz_old.delete_DocumentWriter
def __init__(self, path, options=None):
if type(path) is str:
pass
elif hasattr(path, "absolute"):
path = str(path)
elif hasattr(path, "name"):
path = path.name
if options==None:
options=""
_fitz_old.DocumentWriter_swiginit(self, _fitz_old.new_DocumentWriter(path, options))
def begin_page(self, mediabox):
return _fitz_old.DocumentWriter_begin_page(self, mediabox)
def end_page(self):
return _fitz_old.DocumentWriter_end_page(self)
def close(self):
return _fitz_old.DocumentWriter_close(self)
def __del__(self):
if not type(self) is DocumentWriter:
return
if getattr(self, "thisown", False):
self.__swig_destroy__(self)
def __enter__(self):
return self
def __exit__(self, *args):
self.close()
# Register DocumentWriter in _fitz_old:
_fitz_old.DocumentWriter_swigregister(DocumentWriter)
class Archive(object):
thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag")
__repr__ = _swig_repr
__swig_destroy__ = _fitz_old.delete_Archive
def __init__(self, *args):
self._subarchives = []
_fitz_old.Archive_swiginit(self, _fitz_old.new_Archive(*args))
self.thisown = True
if args != ():
self.add(*args)
def has_entry(self, name):
return _fitz_old.Archive_has_entry(self, name)
def read_entry(self, name):
return _fitz_old.Archive_read_entry(self, name)
def _add_dir(self, folder, path=None):
return _fitz_old.Archive__add_dir(self, folder, path)
def _add_arch(self, subarch, path=None):
return _fitz_old.Archive__add_arch(self, subarch, path)
def _add_ziptarfile(self, filepath, type, path=None):
return _fitz_old.Archive__add_ziptarfile(self, filepath, type, path)
def _add_ziptarmemory(self, memory, type, path=None):
return _fitz_old.Archive__add_ziptarmemory(self, memory, type, path)
def _add_treeitem(self, memory, name, path=None):
return _fitz_old.Archive__add_treeitem(self, memory, name, path)
def add(self, content, path=None):
"""Add a sub-archive.
Args:
content: content to be added. May be one of Archive, folder
name, file name, raw bytes (bytes, bytearray), zipfile,
tarfile, or a sequence of any of these types.
path: (str) a "virtual" path name, under which the elements
of content can be retrieved. Use it to e.g. cope with
duplicate element names.
"""
bin_ok = lambda x: isinstance(x, (bytes, bytearray, io.BytesIO))
entries = []
mount = None
fmt = None
def make_subarch():
subarch = {"fmt": fmt, "entries": entries, "path": mount}
if fmt != "tree" or self._subarchives == []:
self._subarchives.append(subarch)
else:
ltree = self._subarchives[-1]
if ltree["fmt"] != "tree" or ltree["path"] != subarch["path"]:
self._subarchives.append(subarch)
else:
ltree["entries"].extend(subarch["entries"])
self._subarchives[-1] = ltree
return
if isinstance(content, zipfile.ZipFile):
fmt = "zip"
entries = content.namelist()
mount = path
filename = getattr(content, "filename", None)
fp = getattr(content, "fp", None)
if filename:
self._add_ziptarfile(filename, 1, path)
else:
self._add_ziptarmemory(fp.getvalue(), 1, path)
return make_subarch()
if isinstance(content, tarfile.TarFile):
fmt = "tar"
entries = content.getnames()
mount = path
filename = getattr(content.fileobj, "name", None)
fp = content.fileobj
if not isinstance(fp, io.BytesIO) and not filename:
fp = fp.fileobj
if filename:
self._add_ziptarfile(filename, 0, path)
else:
self._add_ziptarmemory(fp.getvalue(), 0, path)
return make_subarch()
if isinstance(content, Archive):
fmt = "multi"
mount = path
self._add_arch(content, path)
return make_subarch()
if bin_ok(content):
if not (path and type(path) is str):
raise ValueError("need name for binary content")
fmt = "tree"
mount = None
entries = [path]
self._add_treeitem(content, path)
return make_subarch()
if hasattr(content, "name"):
content = content.name
elif isinstance(content, pathlib.Path):
content = str(content)
if os.path.isdir(str(content)):
a0 = str(content)
fmt = "dir"
mount = path
entries = os.listdir(a0)
self._add_dir(a0, path)
return make_subarch()
if os.path.isfile(str(content)):
if not (path and type(path) is str):
raise ValueError("need name for binary content")
a0 = str(content)
_ = open(a0, "rb")
ff = _.read()
_.close()
fmt = "tree"
mount = None
entries = [path]
self._add_treeitem(ff, path)
return make_subarch()
if type(content) is str or not getattr(content, "__getitem__", None):
raise ValueError("bad archive content")
#----------------------------------------
# handling sequence types here
#----------------------------------------
if len(content) == 2: # covers the tree item plus path
data, name = content
if bin_ok(data) or os.path.isfile(str(data)):
if not type(name) is str:
raise ValueError(f"bad item name {name}")
mount = path
fmt = "tree"
if bin_ok(data):
self._add_treeitem(data, name, path=mount)
else:
_ = open(str(data), "rb")
ff = _.read()
_.close()
seld._add_treeitem(ff, name, path=mount)
entries = [name]
return make_subarch()
# deal with sequence of disparate items
for item in content:
self.add(item, path)
__doc__ = """Archive(dirname [, path]) - from folder
Archive(file [, path]) - from file name or object
Archive(data, name) - from memory item
Archive() - empty archive
Archive(archive [, path]) - from archive
"""
@property
def entry_list(self):
"""List of sub archives."""
return self._subarchives
def __repr__(self):
return f"Archive, sub-archives: {len(self._subarchives)}"
def __del__(self):
if not type(self) is Archive:
return
if getattr(self, "thisown", False):
self.__swig_destroy__(self)
# Register Archive in _fitz_old:
_fitz_old.Archive_swigregister(Archive)
class Xml(object):
thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag")
__repr__ = _swig_repr
__swig_destroy__ = _fitz_old.delete_Xml
def __init__(self, *args):
_fitz_old.Xml_swiginit(self, _fitz_old.new_Xml(*args))
@property
def root(self):
return _fitz_old.Xml_root(self)
def bodytag(self):
return _fitz_old.Xml_bodytag(self)
def append_child(self, child):
return _fitz_old.Xml_append_child(self, child)
def create_text_node(self, text):
return _fitz_old.Xml_create_text_node(self, text)
def create_element(self, tag):
return _fitz_old.Xml_create_element(self, tag)
def find(self, tag, att, match):
return _fitz_old.Xml_find(self, tag, att, match)
def find_next(self, tag, att, match):
return _fitz_old.Xml_find_next(self, tag, att, match)
@property
def next(self):
return _fitz_old.Xml_next(self)
@property
def previous(self):
return _fitz_old.Xml_previous(self)
def set_attribute(self, key, value):
return _fitz_old.Xml_set_attribute(self, key, value)
def remove_attribute(self, key):
return _fitz_old.Xml_remove_attribute(self, key)
def get_attribute_value(self, key):
return _fitz_old.Xml_get_attribute_value(self, key)
def get_attributes(self):
return _fitz_old.Xml_get_attributes(self)
def insert_before(self, node):
return _fitz_old.Xml_insert_before(self, node)
def insert_after(self, node):
return _fitz_old.Xml_insert_after(self, node)
def clone(self):
return _fitz_old.Xml_clone(self)
@property
def parent(self):
return _fitz_old.Xml_parent(self)
@property
def first_child(self):
return _fitz_old.Xml_first_child(self)
def remove(self):
return _fitz_old.Xml_remove(self)
@property
def text(self):
return _fitz_old.Xml_text(self)
@property
def tagname(self):
return _fitz_old.Xml_tagname(self)
def _get_node_tree(self):
def show_node(node, items, shift):
while node != None:
if node.is_text:
items.append((shift, f'"{node.text}"'))
node = node.next
continue
items.append((shift, f"({node.tagname}"))
for k, v in node.get_attributes().items():
items.append((shift, f"={k} '{v}'"))
child = node.first_child
if child:
items = show_node(child, items, shift + 1)
items.append((shift, f"){node.tagname}"))
node = node.next
return items
shift = 0
items = []
items = show_node(self, items, shift)
return items
def debug(self):
"""Print a list of the node tree below self."""
items = self._get_node_tree()
for item in items:
print(" " * item[0] + item[1].replace("\n", "\\n"))
@property
def is_text(self):
"""Check if this is a text node."""
return self.text != None
@property
def last_child(self):
"""Return last child node."""
child = self.first_child
if child==None:
return None
while True:
if child.next == None:
return child
child = child.next
@staticmethod
def color_text(color):
if type(color) is str:
return color
if type(color) is int:
return f"rgb({sRGB_to_rgb(color)})"
if type(color) in (tuple, list):
return f"rgb{tuple(color)}"
return color
def add_number_list(self, start=1, numtype=None):
"""Add numbered list ("ol" tag)"""
child = self.create_element("ol")
if start > 1:
child.set_attribute("start", str(start))
if numtype != None:
child.set_attribute("type", numtype)
self.append_child(child)
return child
def add_description_list(self):
"""Add description list ("dl" tag)"""
child = self.create_element("dl")
self.append_child(child)
return child
def add_image(self, name, width=None, height=None, imgfloat=None, align=None):
"""Add image node (tag "img")."""
child = self.create_element("img")
if width != None:
child.set_attribute("width", f"{width}")
if height != None:
child.set_attribute("height", f"{height}")
if imgfloat != None:
child.set_attribute("style", f"float: {imgfloat}")
if align != None:
child.set_attribute("align", f"{align}")
child.set_attribute("src", f"{name}")
self.append_child(child)
return child
def add_bullet_list(self):
"""Add bulleted list ("ul" tag)"""
child = self.create_element("ul")
self.append_child(child)
return child
def add_list_item(self):
"""Add item ("li" tag) under a (numbered or bulleted) list."""
if self.tagname not in ("ol", "ul"):
raise ValueError("cannot add list item to", self.tagname)
child = self.create_element("li")
self.append_child(child)
return child
def add_span(self):
child = self.create_element("span")
self.append_child(child)
return child
def add_paragraph(self):
"""Add "p" tag"""
child = self.create_element("p")
if self.tagname != "p":
self.append_child(child)
else:
self.parent.append_child(child)
return child
def add_header(self, level=1):
"""Add header tag"""
if level not in range(1, 7):
raise ValueError("Header level must be in [1, 6]")
this_tag = self.tagname
new_tag = f"h{level}"
child = self.create_element(new_tag)
prev = self
if this_tag not in ("h1", "h2", "h3", "h4", "h5", "h6", "p"):
self.append_child(child)
return child
self.parent.append_child(child)
return child
def add_division(self):
"""Add "div" tag"""
child = self.create_element("div")
self.append_child(child)
return child
def add_horizontal_line(self):
"""Add horizontal line ("hr" tag)"""
child = self.create_element("hr")
self.append_child(child)
return child
def add_link(self, href, text=None):
"""Add a hyperlink ("a" tag)"""
child = self.create_element("a")
if not isinstance(text, str):
text = href
child.set_attribute("href", href)
child.append_child(self.create_text_node(text))
prev = self.span_bottom()
if prev == None:
prev = self
prev.append_child(child)
return self
def add_code(self, text=None):
"""Add a "code" tag"""
child = self.create_element("code")
if type(text) is str:
child.append_child(self.create_text_node(text))
prev = self.span_bottom()
if prev == None:
prev = self
prev.append_child(child)
return self
add_var = add_code
add_samp = add_code
add_kbd = add_code
def add_superscript(self, text=None):
"""Add a superscript ("sup" tag)"""
child = self.create_element("sup")
if type(text) is str:
child.append_child(self.create_text_node(text))
prev = self.span_bottom()
if prev == None:
prev = self
prev.append_child(child)
return self
def add_subscript(self, text=None):
"""Add a subscript ("sub" tag)"""
child = self.create_element("sub")
if type(text) is str:
child.append_child(self.create_text_node(text))
prev = self.span_bottom()
if prev == None:
prev = self
prev.append_child(child)
return self
def add_codeblock(self):
"""Add monospaced lines ("pre" node)"""
child = self.create_element("pre")
self.append_child(child)
return child
def span_bottom(self):
"""Find deepest level in stacked spans."""
parent = self
child = self.last_child
if child == None:
return None
while child.is_text:
child = child.previous
if child == None:
break
if child == None or child.tagname != "span":
return None
while True:
if child == None:
return parent
if child.tagname in ("a", "sub","sup","body") or child.is_text:
child = child.next
continue
if child.tagname == "span":
parent = child
child = child.first_child
else:
return parent
def append_styled_span(self, style):
span = self.create_element("span")
span.add_style(style)
prev = self.span_bottom()
if prev == None:
prev = self
prev.append_child(span)
return prev
def set_margins(self, val):
"""Set margin values via CSS style"""
text = "margins: %s" % val
self.append_styled_span(text)
return self
def set_font(self, font):
"""Set font-family name via CSS style"""
text = "font-family: %s" % font
self.append_styled_span(text)
return self
def set_color(self, color):
"""Set text color via CSS style"""
text = f"color: %s" % self.color_text(color)
self.append_styled_span(text)
return self
def set_columns(self, cols):
"""Set number of text columns via CSS style"""
text = f"columns: {cols}"
self.append_styled_span(text)
return self
def set_bgcolor(self, color):
"""Set background color via CSS style"""
text = f"background-color: %s" % self.color_text(color)
self.add_style(text) # does not work on span level
return self
def set_opacity(self, opacity):
"""Set opacity via CSS style"""
text = f"opacity: {opacity}"
self.append_styled_span(text)
return self
def set_align(self, align):
"""Set text alignment via CSS style"""
text = "text-align: %s"
if isinstance( align, str):
t = align
elif align == TEXT_ALIGN_LEFT:
t = "left"
elif align == TEXT_ALIGN_CENTER:
t = "center"
elif align == TEXT_ALIGN_RIGHT:
t = "right"
elif align == TEXT_ALIGN_JUSTIFY:
t = "justify"
else:
raise ValueError(f"Unrecognised align={align}")
text = text % t
self.add_style(text)
return self
def set_underline(self, val="underline"):
text = "text-decoration: %s" % val
self.append_styled_span(text)
return self
def set_pagebreak_before(self):
"""Insert a page break before this node."""
text = "page-break-before: always"
self.add_style(text)
return self
def set_pagebreak_after(self):
"""Insert a page break after this node."""
text = "page-break-after: always"
self.add_style(text)
return self
def set_fontsize(self, fontsize):
"""Set font size name via CSS style"""
if type(fontsize) is str:
px=""
else:
px="px"
text = f"font-size: {fontsize}{px}"
self.append_styled_span(text)
return self
def set_lineheight(self, lineheight):
"""Set line height name via CSS style - block-level only."""
text = f"line-height: {lineheight}"
self.add_style(text)
return self
def set_leading(self, leading):
"""Set inter-line spacing value via CSS style - block-level only."""
text = f"-mupdf-leading: {leading}"
self.add_style(text)
return self
def set_word_spacing(self, spacing):
"""Set inter-word spacing value via CSS style"""
text = f"word-spacing: {spacing}"
self.append_styled_span(text)
return self
def set_letter_spacing(self, spacing):
"""Set inter-letter spacing value via CSS style"""
text = f"letter-spacing: {spacing}"
self.append_styled_span(text)
return self
def set_text_indent(self, indent):
"""Set text indentation name via CSS style - block-level only."""
text = f"text-indent: {indent}"
self.add_style(text)
return self
def set_bold(self, val=True):
"""Set bold on / off via CSS style"""
if val:
val="bold"
else:
val="normal"
text = "font-weight: %s" % val
self.append_styled_span(text)
return self
def set_italic(self, val=True):
"""Set italic on / off via CSS style"""
if val:
val="italic"
else:
val="normal"
text = "font-style: %s" % val
self.append_styled_span(text)
return self
def set_properties(
self,
align=None,
bgcolor=None,
bold=None,
color=None,
columns=None,
font=None,
fontsize=None,
indent=None,
italic=None,
leading=None,
letter_spacing=None,
lineheight=None,
margins=None,
pagebreak_after=None,
pagebreak_before=None,
word_spacing=None,
unqid=None,
cls=None,
):
"""Set any or all properties of a node.
To be used for existing nodes preferrably.
"""
root = self.root
temp = root.add_division()
if align is not None:
temp.set_align(align)
if bgcolor is not None:
temp.set_bgcolor(bgcolor)
if bold is not None:
temp.set_bold(bold)
if color is not None:
temp.set_color(color)
if columns is not None:
temp.set_columns(columns)
if font is not None:
temp.set_font(font)
if fontsize is not None:
temp.set_fontsize(fontsize)
if indent is not None:
temp.set_text_indent(indent)
if italic is not None:
temp.set_italic(italic)
if leading is not None:
temp.set_leading(leading)
if letter_spacing is not None:
temp.set_letter_spacing(letter_spacing)
if lineheight is not None:
temp.set_lineheight(lineheight)
if margins is not None:
temp.set_margins(margins)
if pagebreak_after is not None:
temp.set_pagebreak_after()
if pagebreak_before is not None:
temp.set_pagebreak_before()
if word_spacing is not None:
temp.set_word_spacing(word_spacing)
if unqid is not None:
self.set_id(unqid)
if cls is not None:
self.add_class(cls)
styles = []
top_style = temp.get_attribute_value("style")
if top_style is not None:
styles.append(top_style)
child = temp.first_child
while child:
styles.append(child.get_attribute_value("style"))
child = child.first_child
self.set_attribute("style", ";".join(styles))
temp.remove()
return self
def set_id(self, unique):
"""Set a unique id."""
# check uniqueness
tagname = self.tagname
root = self.root
if root.find(None, "id", unique):
raise ValueError(f"id '{unique}' already exists")
self.set_attribute("id", unique)
return self
def add_text(self, text):
"""Add text. Line breaks are honored."""
lines = text.splitlines()
line_count = len(lines)
prev = self.span_bottom()
if prev == None:
prev = self
for i, line in enumerate(lines):
prev.append_child(self.create_text_node(line))
if i < line_count - 1:
prev.append_child(self.create_element("br"))
return self
def add_style(self, text):
"""Set some style via CSS style. Replaces complete style spec."""
style = self.get_attribute_value("style")
if style != None and text in style:
return self
self.remove_attribute("style")
if style == None:
style = text
else:
style += ";" + text
self.set_attribute("style", style)
return self
def add_class(self, text):
"""Set some class via CSS. Replaces complete class spec."""
cls = self.get_attribute_value("class")
if cls != None and text in cls:
return self
self.remove_attribute("class")
if cls == None:
cls = text
else:
cls += " " + text
self.set_attribute("class", cls)
return self
def insert_text(self, text):
lines = text.splitlines()
line_count = len(lines)
for i, line in enumerate(lines):
self.append_child(self.create_text_node(line))
if i < line_count - 1:
self.append_child(self.create_element("br"))
return self
def __enter__(self):
return self
def __exit__(self, *args):
pass
def __del__(self):
if not type(self) is Xml:
return
if getattr(self, "thisown", False):
self.__swig_destroy__(self)
# Register Xml in _fitz_old:
_fitz_old.Xml_swigregister(Xml)
class Story(object):
thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag")
__repr__ = _swig_repr
__swig_destroy__ = _fitz_old.delete_Story
def __init__(self, html=None, user_css=None, em=12, archive=None):
if archive != None and isinstance(archive, Archive) == False:
archive = Archive(archive)
_fitz_old.Story_swiginit(self, _fitz_old.new_Story(html, user_css, em, archive))
def reset(self):
return _fitz_old.Story_reset(self)
def place(self, where):
return _fitz_old.Story_place(self, where)
def draw(self, device, matrix=None):
return _fitz_old.Story_draw(self, device, matrix)
def document(self):
return _fitz_old.Story_document(self)
def element_positions(self, function, args):
"""Trigger a callback function to record where items have been placed.
Args:
function: a function accepting exactly one argument.
args: an optional dictionary for passing additional data.
"""
if type(args) is dict:
for k in args.keys():
if not (type(k) is str and k.isidentifier()):
raise ValueError(f"invalid key '{k}'")
else:
args = {}
if not callable(function) or function.__code__.co_argcount != 1:
raise ValueError("callback 'function' must be a callable with exactly one argument")
return _fitz_old.Story_element_positions(self, function, args)
def write(self, writer, rectfn, positionfn=None, pagefn=None):
dev = None
page_num = 0
rect_num = 0
filled = Rect(0, 0, 0, 0)
while 1:
mediabox, rect, ctm = rectfn(rect_num, filled)
rect_num += 1
if mediabox:
# new page.
page_num += 1
more, filled = self.place( rect)
#print(f"write(): positionfn={positionfn}")
if positionfn:
def positionfn2(position):
# We add a `.page_num` member to the
# `ElementPosition` instance.
position.page_num = page_num
#print(f"write(): position={position}")
positionfn(position)
self.element_positions(positionfn2, {})
if writer:
if mediabox:
# new page.
if dev:
if pagefn:
pagefn(page_num, medibox, dev, 1)
writer.end_page()
dev = writer.begin_page( mediabox)
if pagefn:
pagefn(page_num, mediabox, dev, 0)
self.draw( dev, ctm)
if not more:
if pagefn:
pagefn( page_num, mediabox, dev, 1)
writer.end_page()
else:
self.draw(None, ctm)
if not more:
break
@staticmethod
def write_stabilized(writer, contentfn, rectfn, user_css=None, em=12, positionfn=None, pagefn=None, archive=None, add_header_ids=True):
positions = list()
content = None
# Iterate until stable.
while 1:
content_prev = content
content = contentfn( positions)
stable = False
if content == content_prev:
stable = True
content2 = content
story = Story(content2, user_css, em, archive)
if add_header_ids:
story.add_header_ids()
positions = list()
def positionfn2(position):
#print(f"write_stabilized(): stable={stable} positionfn={positionfn} position={position}")
positions.append(position)
if stable and positionfn:
positionfn(position)
story.write(
writer if stable else None,
rectfn,
positionfn2,
pagefn,
)
if stable:
break
def add_header_ids(self):
'''
Look for `<h1..6>` items in `self` and adds unique `id`
attributes if not already present.
'''
dom = self.body
i = 0
x = dom.find(None, None, None)
while x:
name = x.tagname
if len(name) == 2 and name[0]=="h" and name[1] in "123456":
attr = x.get_attribute_value("id")
if not attr:
id_ = f"h_id_{i}"
#print(f"name={name}: setting id={id_}")
x.set_attribute("id", id_)
i += 1
x = x.find_next(None, None, None)
def write_with_links(self, rectfn, positionfn=None, pagefn=None):
#print("write_with_links()")
stream = io.BytesIO()
writer = DocumentWriter(stream)
positions = []
def positionfn2(position):
#print(f"write_with_links(): position={position}")
positions.append(position)
if positionfn:
positionfn(position)
self.write(writer, rectfn, positionfn=positionfn2, pagefn=pagefn)
writer.close()
stream.seek(0)
return Story.add_pdf_links(stream, positions)
@staticmethod
def write_stabilized_with_links(contentfn, rectfn, user_css=None, em=12, positionfn=None, pagefn=None, archive=None, add_header_ids=True):
#print("write_stabilized_with_links()")
stream = io.BytesIO()
writer = DocumentWriter(stream)
positions = []
def positionfn2(position):
#print(f"write_stabilized_with_links(): position={position}")
positions.append(position)
if positionfn:
positionfn(position)
Story.write_stabilized(writer, contentfn, rectfn, user_css, em, positionfn2, pagefn, archive, add_header_ids)
writer.close()
stream.seek(0)
return Story.add_pdf_links(stream, positions)
@staticmethod
def add_pdf_links(document_or_stream, positions):
"""
Adds links to PDF document.
Args:
document_or_stream:
A PDF `Document` or raw PDF content, for example an
`io.BytesIO` instance.
positions:
List of `ElementPosition`'s for `document_or_stream`,
typically from Story.element_positions(). We raise an
exception if two or more positions have same id.
Returns:
`document_or_stream` if a `Document` instance, otherwise a
new `Document` instance.
We raise an exception if an `href` in `positions` refers to an
internal position `#<name>` but no item in `postions` has `id =
name`.
"""
if isinstance(document_or_stream, Document):
document = document_or_stream
else:
document = Document("pdf", document_or_stream)
# Create dict from id to position, which we will use to find
# link destinations.
#
id_to_position = dict()
#print(f"positions: {positions}")
for position in positions:
#print(f"add_pdf_links(): position: {position}")
if (position.open_close & 1) and position.id:
#print(f"add_pdf_links(): position with id: {position}")
if position.id in id_to_position:
#print(f"Ignoring duplicate positions with id={position.id!r}")
pass
else:
id_to_position[ position.id] = position
# Insert links for all positions that have an `href` starting
# with '#'.
#
for position_from in positions:
if ((position_from.open_close & 1)
and position_from.href
and position_from.href.startswith("#")
):
# This is a `<a href="#...">...</a>` internal link.
#print(f"add_pdf_links(): position with href: {position}")
target_id = position_from.href[1:]
try:
position_to = id_to_position[ target_id]
except Exception as e:
raise RuntimeError(f"No destination with id={target_id}, required by position_from: {position_from}")
# Make link from `position_from`'s rect to top-left of
# `position_to`'s rect.
if 0:
print(f"add_pdf_links(): making link from:")
print(f"add_pdf_links(): {position_from}")
print(f"add_pdf_links(): to:")
print(f"add_pdf_links(): {position_to}")
link = dict()
link["kind"] = LINK_GOTO
link["from"] = Rect(position_from.rect)
x0, y0, x1, y1 = position_to.rect
# This appears to work well with viewers which scroll
# to make destination point top-left of window.
link["to"] = Point(x0, y0)
link["page"] = position_to.page_num - 1
document[position_from.page_num - 1].insert_link(link)
return document
@property
def body(self):
dom = self.document()
return dom.bodytag()
def __del__(self):
if not type(self) is Story:
return
if getattr(self, "thisown", False):
self.__swig_destroy__(self)
# Register Story in _fitz_old:
_fitz_old.Story_swigregister(Story)
class Tools(object):
thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag")
__repr__ = _swig_repr
def __init__(self):
_fitz_old.Tools_swiginit(self, _fitz_old.new_Tools())
__swig_destroy__ = _fitz_old.delete_Tools
def gen_id(self):
"""Return a unique positive integer."""
return _fitz_old.Tools_gen_id(self)
def set_icc(self, on=0):
"""Set ICC color handling on or off."""
return _fitz_old.Tools_set_icc(self, on)
def set_annot_stem(self, stem=None):
"""Get / set id prefix for annotations."""
return _fitz_old.Tools_set_annot_stem(self, stem)
def set_small_glyph_heights(self, on=None):
"""Set / unset small glyph heights."""
return _fitz_old.Tools_set_small_glyph_heights(self, on)
def set_subset_fontnames(self, on=None):
"""Set / unset returning fontnames with their subset prefix."""
return _fitz_old.Tools_set_subset_fontnames(self, on)
def set_low_memory(self, on=None):
"""Set / unset MuPDF device caching."""
return _fitz_old.Tools_set_low_memory(self, on)
def unset_quad_corrections(self, on=None):
"""Set ascender / descender corrections on or off."""
return _fitz_old.Tools_unset_quad_corrections(self, on)
def store_shrink(self, percent):
"""Free 'percent' of current store size."""
return _fitz_old.Tools_store_shrink(self, percent)
@property
def store_size(self):
"""MuPDF current store size."""
return _fitz_old.Tools_store_size(self)
@property
def store_maxsize(self):
"""MuPDF store size limit."""
return _fitz_old.Tools_store_maxsize(self)
def show_aa_level(self):
"""Show anti-aliasing values."""
val = _fitz_old.Tools_show_aa_level(self)
temp = {"graphics": val[0], "text": val[1], "graphics_min_line_width": val[2]}
val = temp
return val
def set_aa_level(self, level):
"""Set anti-aliasing level."""
return _fitz_old.Tools_set_aa_level(self, level)
def set_graphics_min_line_width(self, min_line_width):
"""Set the graphics minimum line width."""
return _fitz_old.Tools_set_graphics_min_line_width(self, min_line_width)
def image_profile(self, stream, keep_image=0):
"""Metadata of an image binary stream."""
return _fitz_old.Tools_image_profile(self, stream, keep_image)
def _rotate_matrix(self, page):
return _fitz_old.Tools__rotate_matrix(self, page)
def _derotate_matrix(self, page):
return _fitz_old.Tools__derotate_matrix(self, page)
@property
def fitz_config(self):
"""PyMuPDF configuration parameters."""
return _fitz_old.Tools_fitz_config(self)
def glyph_cache_empty(self):
"""Empty the glyph cache."""
return _fitz_old.Tools_glyph_cache_empty(self)
def _fill_widget(self, annot, widget):
val = _fitz_old.Tools__fill_widget(self, annot, widget)
widget.rect = Rect(annot.rect)
widget.xref = annot.xref
widget.parent = annot.parent
widget._annot = annot # backpointer to annot object
if not widget.script:
widget.script = None
if not widget.script_stroke:
widget.script_stroke = None
if not widget.script_format:
widget.script_format = None
if not widget.script_change:
widget.script_change = None
if not widget.script_calc:
widget.script_calc = None
if not widget.script_blur:
widget.script_blur = None
if not widget.script_focus:
widget.script_focus = None
return val
def _save_widget(self, annot, widget):
return _fitz_old.Tools__save_widget(self, annot, widget)
def _reset_widget(self, annot):
return _fitz_old.Tools__reset_widget(self, annot)
def _ensure_widget_calc(self, annot):
return _fitz_old.Tools__ensure_widget_calc(self, annot)
def _parse_da(self, annot):
val = _fitz_old.Tools__parse_da(self, annot)
if not val:
return ((0,), "", 0)
font = "Helv"
fsize = 12
col = (0, 0, 0)
dat = val.split() # split on any whitespace
for i, item in enumerate(dat):
if item == "Tf":
font = dat[i - 2][1:]
fsize = float(dat[i - 1])
dat[i] = dat[i-1] = dat[i-2] = ""
continue
if item == "g": # unicolor text
col = [(float(dat[i - 1]))]
dat[i] = dat[i-1] = ""
continue
if item == "rg": # RGB colored text
col = [float(f) for f in dat[i - 3:i]]
dat[i] = dat[i-1] = dat[i-2] = dat[i-3] = ""
continue
if item == "k": # CMYK colored text
col = [float(f) for f in dat[i - 4:i]]
dat[i] = dat[i-1] = dat[i-2] = dat[i-3] = dat[i-4] = ""
continue
val = (col, font, fsize)
return val
def _update_da(self, annot, da_str):
return _fitz_old.Tools__update_da(self, annot, da_str)
def _get_all_contents(self, fzpage):
"""Concatenate all /Contents objects of a page into a bytes object."""
return _fitz_old.Tools__get_all_contents(self, fzpage)
def _insert_contents(self, page, newcont, overlay=1):
"""Add bytes as a new /Contents object for a page, and return its xref."""
return _fitz_old.Tools__insert_contents(self, page, newcont, overlay)
def mupdf_version(self):
"""Get version of MuPDF binary build."""
return _fitz_old.Tools_mupdf_version(self)
def mupdf_warnings(self, reset=1):
"""Get the MuPDF warnings/errors with optional reset (default)."""
val = _fitz_old.Tools_mupdf_warnings(self, reset)
val = "\n".join(val)
if reset:
self.reset_mupdf_warnings()
return val
def _int_from_language(self, language):
return _fitz_old.Tools__int_from_language(self, language)
def reset_mupdf_warnings(self):
"""Empty the MuPDF warnings/errors store."""
return _fitz_old.Tools_reset_mupdf_warnings(self)
def mupdf_display_errors(self, on=None):
"""Set MuPDF error display to True or False."""
return _fitz_old.Tools_mupdf_display_errors(self, on)
def mupdf_display_warnings(self, on=None):
"""Set MuPDF warnings display to True or False."""
return _fitz_old.Tools_mupdf_display_warnings(self, on)
def _le_annot_parms(self, annot, p1, p2, fill_color):
"""Get common parameters for making annot line end symbols.
Returns:
m: matrix that maps p1, p2 to points L, P on the x-axis
im: its inverse
L, P: transformed p1, p2
w: line width
scol: stroke color string
fcol: fill color store_shrink
opacity: opacity string (gs command)
"""
w = annot.border["width"] # line width
sc = annot.colors["stroke"] # stroke color
if not sc: # black if missing
sc = (0,0,0)
scol = " ".join(map(str, sc)) + " RG\n"
if fill_color:
fc = fill_color
else:
fc = annot.colors["fill"] # fill color
if not fc:
fc = (1,1,1) # white if missing
fcol = " ".join(map(str, fc)) + " rg\n"
# nr = annot.rect
np1 = p1 # point coord relative to annot rect
np2 = p2 # point coord relative to annot rect
m = Matrix(util_hor_matrix(np1, np2)) # matrix makes the line horizontal
im = ~m # inverted matrix
L = np1 * m # converted start (left) point
R = np2 * m # converted end (right) point
if 0 <= annot.opacity < 1:
opacity = "/H gs\n"
else:
opacity = ""
return m, im, L, R, w, scol, fcol, opacity
def _oval_string(self, p1, p2, p3, p4):
"""Return /AP string defining an oval within a 4-polygon provided as points
"""
def bezier(p, q, r):
f = "%f %f %f %f %f %f c\n"
return f % (p.x, p.y, q.x, q.y, r.x, r.y)
kappa = 0.55228474983 # magic number
ml = p1 + (p4 - p1) * 0.5 # middle points ...
mo = p1 + (p2 - p1) * 0.5 # for each ...
mr = p2 + (p3 - p2) * 0.5 # polygon ...
mu = p4 + (p3 - p4) * 0.5 # side
ol1 = ml + (p1 - ml) * kappa # the 8 bezier
ol2 = mo + (p1 - mo) * kappa # helper points
or1 = mo + (p2 - mo) * kappa
or2 = mr + (p2 - mr) * kappa
ur1 = mr + (p3 - mr) * kappa
ur2 = mu + (p3 - mu) * kappa
ul1 = mu + (p4 - mu) * kappa
ul2 = ml + (p4 - ml) * kappa
# now draw, starting from middle point of left side
ap = "%f %f m\n" % (ml.x, ml.y)
ap += bezier(ol1, ol2, mo)
ap += bezier(or1, or2, mr)
ap += bezier(ur1, ur2, mu)
ap += bezier(ul1, ul2, ml)
return ap
def _le_diamond(self, annot, p1, p2, lr, fill_color):
"""Make stream commands for diamond line end symbol. "lr" denotes left (False) or right point.
"""
m, im, L, R, w, scol, fcol, opacity = self._le_annot_parms(annot, p1, p2, fill_color)
shift = 2.5 # 2*shift*width = length of square edge
d = shift * max(1, w)
M = R - (d/2., 0) if lr else L + (d/2., 0)
r = Rect(M, M) + (-d, -d, d, d) # the square
# the square makes line longer by (2*shift - 1)*width
p = (r.tl + (r.bl - r.tl) * 0.5) * im
ap = "q\n%s%f %f m\n" % (opacity, p.x, p.y)
p = (r.tl + (r.tr - r.tl) * 0.5) * im
ap += "%f %f l\n" % (p.x, p.y)
p = (r.tr + (r.br - r.tr) * 0.5) * im
ap += "%f %f l\n" % (p.x, p.y)
p = (r.br + (r.bl - r.br) * 0.5) * im
ap += "%f %f l\n" % (p.x, p.y)
ap += "%g w\n" % w
ap += scol + fcol + "b\nQ\n"
return ap
def _le_square(self, annot, p1, p2, lr, fill_color):
"""Make stream commands for square line end symbol. "lr" denotes left (False) or right point.
"""
m, im, L, R, w, scol, fcol, opacity = self._le_annot_parms(annot, p1, p2, fill_color)
shift = 2.5 # 2*shift*width = length of square edge
d = shift * max(1, w)
M = R - (d/2., 0) if lr else L + (d/2., 0)
r = Rect(M, M) + (-d, -d, d, d) # the square
# the square makes line longer by (2*shift - 1)*width
p = r.tl * im
ap = "q\n%s%f %f m\n" % (opacity, p.x, p.y)
p = r.tr * im
ap += "%f %f l\n" % (p.x, p.y)
p = r.br * im
ap += "%f %f l\n" % (p.x, p.y)
p = r.bl * im
ap += "%f %f l\n" % (p.x, p.y)
ap += "%g w\n" % w
ap += scol + fcol + "b\nQ\n"
return ap
def _le_circle(self, annot, p1, p2, lr, fill_color):
"""Make stream commands for circle line end symbol. "lr" denotes left (False) or right point.
"""
m, im, L, R, w, scol, fcol, opacity = self._le_annot_parms(annot, p1, p2, fill_color)
shift = 2.5 # 2*shift*width = length of square edge
d = shift * max(1, w)
M = R - (d/2., 0) if lr else L + (d/2., 0)
r = Rect(M, M) + (-d, -d, d, d) # the square
ap = "q\n" + opacity + self._oval_string(r.tl * im, r.tr * im, r.br * im, r.bl * im)
ap += "%g w\n" % w
ap += scol + fcol + "b\nQ\n"
return ap
def _le_butt(self, annot, p1, p2, lr, fill_color):
"""Make stream commands for butt line end symbol. "lr" denotes left (False) or right point.
"""
m, im, L, R, w, scol, fcol, opacity = self._le_annot_parms(annot, p1, p2, fill_color)
shift = 3
d = shift * max(1, w)
M = R if lr else L
top = (M + (0, -d/2.)) * im
bot = (M + (0, d/2.)) * im
ap = "\nq\n%s%f %f m\n" % (opacity, top.x, top.y)
ap += "%f %f l\n" % (bot.x, bot.y)
ap += "%g w\n" % w
ap += scol + "s\nQ\n"
return ap
def _le_slash(self, annot, p1, p2, lr, fill_color):
"""Make stream commands for slash line end symbol. "lr" denotes left (False) or right point.
"""
m, im, L, R, w, scol, fcol, opacity = self._le_annot_parms(annot, p1, p2, fill_color)
rw = 1.1547 * max(1, w) * 1.0 # makes rect diagonal a 30 deg inclination
M = R if lr else L
r = Rect(M.x - rw, M.y - 2 * w, M.x + rw, M.y + 2 * w)
top = r.tl * im
bot = r.br * im
ap = "\nq\n%s%f %f m\n" % (opacity, top.x, top.y)
ap += "%f %f l\n" % (bot.x, bot.y)
ap += "%g w\n" % w
ap += scol + "s\nQ\n"
return ap
def _le_openarrow(self, annot, p1, p2, lr, fill_color):
"""Make stream commands for open arrow line end symbol. "lr" denotes left (False) or right point.
"""
m, im, L, R, w, scol, fcol, opacity = self._le_annot_parms(annot, p1, p2, fill_color)
shift = 2.5
d = shift * max(1, w)
p2 = R + (d/2., 0) if lr else L - (d/2., 0)
p1 = p2 + (-2*d, -d) if lr else p2 + (2*d, -d)
p3 = p2 + (-2*d, d) if lr else p2 + (2*d, d)
p1 *= im
p2 *= im
p3 *= im
ap = "\nq\n%s%f %f m\n" % (opacity, p1.x, p1.y)
ap += "%f %f l\n" % (p2.x, p2.y)
ap += "%f %f l\n" % (p3.x, p3.y)
ap += "%g w\n" % w
ap += scol + "S\nQ\n"
return ap
def _le_closedarrow(self, annot, p1, p2, lr, fill_color):
"""Make stream commands for closed arrow line end symbol. "lr" denotes left (False) or right point.
"""
m, im, L, R, w, scol, fcol, opacity = self._le_annot_parms(annot, p1, p2, fill_color)
shift = 2.5
d = shift * max(1, w)
p2 = R + (d/2., 0) if lr else L - (d/2., 0)
p1 = p2 + (-2*d, -d) if lr else p2 + (2*d, -d)
p3 = p2 + (-2*d, d) if lr else p2 + (2*d, d)
p1 *= im
p2 *= im
p3 *= im
ap = "\nq\n%s%f %f m\n" % (opacity, p1.x, p1.y)
ap += "%f %f l\n" % (p2.x, p2.y)
ap += "%f %f l\n" % (p3.x, p3.y)
ap += "%g w\n" % w
ap += scol + fcol + "b\nQ\n"
return ap
def _le_ropenarrow(self, annot, p1, p2, lr, fill_color):
"""Make stream commands for right open arrow line end symbol. "lr" denotes left (False) or right point.
"""
m, im, L, R, w, scol, fcol, opacity = self._le_annot_parms(annot, p1, p2, fill_color)
shift = 2.5
d = shift * max(1, w)
p2 = R - (d/3., 0) if lr else L + (d/3., 0)
p1 = p2 + (2*d, -d) if lr else p2 + (-2*d, -d)
p3 = p2 + (2*d, d) if lr else p2 + (-2*d, d)
p1 *= im
p2 *= im
p3 *= im
ap = "\nq\n%s%f %f m\n" % (opacity, p1.x, p1.y)
ap += "%f %f l\n" % (p2.x, p2.y)
ap += "%f %f l\n" % (p3.x, p3.y)
ap += "%g w\n" % w
ap += scol + fcol + "S\nQ\n"
return ap
def _le_rclosedarrow(self, annot, p1, p2, lr, fill_color):
"""Make stream commands for right closed arrow line end symbol. "lr" denotes left (False) or right point.
"""
m, im, L, R, w, scol, fcol, opacity = self._le_annot_parms(annot, p1, p2, fill_color)
shift = 2.5
d = shift * max(1, w)
p2 = R - (2*d, 0) if lr else L + (2*d, 0)
p1 = p2 + (2*d, -d) if lr else p2 + (-2*d, -d)
p3 = p2 + (2*d, d) if lr else p2 + (-2*d, d)
p1 *= im
p2 *= im
p3 *= im
ap = "\nq\n%s%f %f m\n" % (opacity, p1.x, p1.y)
ap += "%f %f l\n" % (p2.x, p2.y)
ap += "%f %f l\n" % (p3.x, p3.y)
ap += "%g w\n" % w
ap += scol + fcol + "b\nQ\n"
return ap
def __del__(self):
if not type(self) is Tools:
return
if getattr(self, "thisown", False):
self.__swig_destroy__(self)
# Register Tools in _fitz_old:
_fitz_old.Tools_swigregister(Tools)
Back to Directory
File Manager