'''
PyMuPDF implemented on top of MuPDF Python bindings.
License:
SPDX-License-Identifier: GPL-3.0-only
'''
import atexit
import binascii
import glob
import inspect
import io
import math
import os
import pathlib
import re
import string
import sys
import tarfile
import typing
import warnings
import weakref
import zipfile
from . import extra
def log( text, caller=1):
frame_record = inspect.stack( context=0)[ caller]
filename = os.path.relpath(frame_record.filename)
line = frame_record.lineno
function = frame_record.function
print( f'{filename}:{line}:{function}: {text}', file=sys.stdout)
sys.stdout.flush()
if os.path.exists( 'fitz/__init__.py'):
if not glob.glob( 'fitz/_extra*') or not glob.glob( 'fitz/_mupdf*'):
log( '#' * 40)
log( '# Warning: current directory appears to contain an incomplete')
log( '# fitz/ installation directory so "import fitz" may fail.')
log( '# This can happen if current directory is a PyMuPDF source tree.')
log( '# Suggest changing to a different current directory.')
log( '#' * 40)
def exception_info():
import traceback
log(f'exception_info:')
traceback.print_exc(file=sys.stdout)
INVALID_NAME_CHARS = set(string.whitespace + "()<>[]{}/%" + chr(0))
def get_env_bool( name, default):
'''
Returns `True`, `False` or `default` depending on whether $<name> is '1',
'0' or unset. Otherwise assert-fails.
'''
v = os.environ.get( name)
if v is None:
ret = default
elif v == '1':
ret = True
elif v == '0':
ret = False
else:
assert 0, f'Unrecognised value for {name}: {v!r}'
if ret != default:
log(f'Using non-default setting from {name}: {v!r}')
return ret
g_exceptions_verbose = get_env_bool( 'PYMUPDF_EXCEPTIONS_VERBOSE', True)
g_use_extra = get_env_bool( 'PYMUPDF_USE_EXTRA', True)
g_no_device_caching = 0
g_small_glyph_heights = 0
g_subset_fontnames = 0
g_skip_quad_corrections = 0
mupdf_cppyy = os.environ.get( 'MUPDF_CPPYY')
if mupdf_cppyy is not None:
log( f'{__file__}: $MUPDF_CPPYY={mupdf_cppyy!r} so attempting to import mupdf_cppyy.')
log( f'{__file__}: $PYTHONPATH={os.environ["PYTHONPATH"]}')
if mupdf_cppyy == '':
import mupdf_cppyy
else:
import importlib
mupdf_cppyy = importlib.machinery.SourceFileLoader(
'mupdf_cppyy',
mupdf_cppyy
).load_module()
mupdf = mupdf_cppyy.cppyy.gbl.mupdf
else:
try:
from . import mupdf
except Exception:
import mupdf
mupdf.reinit_singlethreaded()
mupdf_version_tuple = (mupdf.FZ_VERSION_MAJOR, mupdf.FZ_VERSION_MINOR, mupdf.FZ_VERSION_PATCH)
OptBytes = typing.Optional[typing.ByteString]
OptDict = typing.Optional[dict]
OptFloat = typing.Optional[float]
OptInt = typing.Union[int, None]
OptSeq = typing.Optional[typing.Sequence]
OptStr = typing.Optional[str]
Page = 'Page_forward_decl'
Point = 'Point_forward_decl'
TESSDATA_PREFIX = os.environ.get("TESSDATA_PREFIX")
matrix_like = 'matrix_like'
point_like = 'point_like'
quad_like = 'quad_like'
rect_like = 'rect_like'
def _as_fz_document(document):
'''
Returns document as a mupdf.FzDocument, upcasting as required.
'''
if isinstance(document, Document):
return _as_fz_document(document.this)
elif isinstance(document, mupdf.FzDocument):
return document
elif isinstance(document, mupdf.PdfDocument):
return document.super()
else:
assert 0, f'Unrecognised {type(document)=}'
def _as_pdf_document(document):
'''
Returns document as a mupdf.PdfDocument, downcasting as required. If we
fail (i.e. document is a mupdf.FzDocument(), <ret>.m_internal will be None.
'''
if isinstance(document, Document):
return _as_pdf_document(document.this)
elif isinstance(document, mupdf.PdfDocument):
return document
if isinstance(document, mupdf.FzDocument):
return mupdf.PdfDocument(document)
assert 0, f'Unrecognised {type(document)=}'
def _as_fz_page(page):
'''
Returns page as a mupdf.FzPage, upcasting as required.
'''
if isinstance(page, Page):
page = page.this
if isinstance(page, mupdf.PdfPage):
return page.super()
elif isinstance(page, mupdf.FzPage):
return page
assert 0, f'Unrecognised {type(page)=}'
JM_MEMORY = 0
class Annot:
def __init__(self, annot):
assert isinstance( annot, mupdf.PdfAnnot)
self.this = annot
def __repr__(self):
parent = getattr(self, 'parent', '<>')
return "'%s' annotation on %s" % (self.type[1], str(parent))
def __str__(self):
return self.__repr__()
def _erase(self):
if getattr(self, "thisown", False):
self.thisown = False
def _get_redact_values(self):
annot = self.this
if mupdf.pdf_annot_type(annot) != mupdf.PDF_ANNOT_REDACT:
return
values = dict()
try:
obj = mupdf.pdf_dict_gets(mupdf.pdf_annot_obj(annot), "RO")
if obj.m_internal:
JM_Warning("Ignoring redaction key '/RO'.")
xref = mupdf.pdf_to_num(obj)
values[dictkey_xref] = xref
obj = mupdf.pdf_dict_gets(mupdf.pdf_annot_obj(annot), "OverlayText")
if obj.m_internal:
text = mupdf.pdf_to_text_string(obj)
values[dictkey_text] = JM_UnicodeFromStr(text)
else:
values[dictkey_text] = ''
obj = mupdf.pdf_dict_get(mupdf.pdf_annot_obj(annot), PDF_NAME('Q'))
align = 0
if obj.m_internal:
align = mupdf.pdf_to_int(obj)
values[dictkey_align] = align
except Exception:
if g_exceptions_verbose: exception_info()
return
val = values
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 _getAP(self):
if g_use_extra:
assert isinstance( self.this, mupdf.PdfAnnot)
ret = extra.Annot_getAP(self.this)
assert isinstance( ret, bytes)
return ret
else:
r = None
res = None
annot = self.this
assert isinstance( annot, mupdf.PdfAnnot)
annot_obj = mupdf.pdf_annot_obj( annot)
ap = mupdf.pdf_dict_getl( annot_obj, PDF_NAME('AP'), PDF_NAME('N'))
if mupdf.pdf_is_stream( ap):
res = mupdf.pdf_load_stream( ap)
if res and res.m_internal:
r = JM_BinFromBuffer(res)
return r
def _setAP(self, buffer_, rect=0):
try:
annot = self.this
annot_obj = mupdf.pdf_annot_obj( annot)
page = mupdf.pdf_annot_page( annot)
apobj = mupdf.pdf_dict_getl( annot_obj, PDF_NAME('AP'), PDF_NAME('N'))
if not apobj.m_internal:
raise RuntimeError( MSG_BAD_APN)
if not mupdf.pdf_is_stream( apobj):
raise RuntimeError( MSG_BAD_APN)
res = JM_BufferFromBytes( buffer_)
if not res.m_internal:
raise ValueError( MSG_BAD_BUFFER)
JM_update_stream( page.doc(), apobj, res, 1)
if rect:
bbox = mupdf.pdf_dict_get_rect( annot_obj, PDF_NAME('Rect'))
mupdf.pdf_dict_put_rect( apobj, PDF_NAME('BBox'), bbox)
except Exception:
if g_exceptions_verbose: exception_info()
def _update_appearance(self, opacity=-1, blend_mode=None, fill_color=None, rotate=-1):
annot = self.this
assert annot.m_internal
annot_obj = mupdf.pdf_annot_obj( annot)
page = mupdf.pdf_annot_page( annot)
pdf = page.doc()
type_ = mupdf.pdf_annot_type( annot)
nfcol, fcol = JM_color_FromSequence(fill_color)
try:
if nfcol == 0 or type_ not in (
mupdf.PDF_ANNOT_SQUARE,
mupdf.PDF_ANNOT_CIRCLE,
mupdf.PDF_ANNOT_LINE,
mupdf.PDF_ANNOT_POLY_LINE,
mupdf.PDF_ANNOT_POLYGON
):
mupdf.pdf_dict_del( annot_obj, PDF_NAME('IC'))
elif nfcol > 0:
mupdf.pdf_set_annot_interior_color( annot, fcol[:nfcol])
insert_rot = 1 if rotate >= 0 else 0
if type_ not in (
mupdf.PDF_ANNOT_CARET,
mupdf.PDF_ANNOT_CIRCLE,
mupdf.PDF_ANNOT_FREE_TEXT,
mupdf.PDF_ANNOT_FILE_ATTACHMENT,
mupdf.PDF_ANNOT_INK,
mupdf.PDF_ANNOT_LINE,
mupdf.PDF_ANNOT_POLY_LINE,
mupdf.PDF_ANNOT_POLYGON,
mupdf.PDF_ANNOT_SQUARE,
mupdf.PDF_ANNOT_STAMP,
mupdf.PDF_ANNOT_TEXT,
):
insert_rot = 0
if insert_rot:
mupdf.pdf_dict_put_int( annot_obj, PDF_NAME('Rotate'), rotate)
mupdf.pdf_dirty_annot( annot)
mupdf.pdf_update_annot( annot)
pdf.resynth_required = 0
if type_ == mupdf.PDF_ANNOT_FREE_TEXT:
if nfcol > 0:
mupdf.pdf_set_annot_color( annot, fcol[:nfcol])
elif nfcol > 0:
col = mupdf.pdf_new_array( page.doc(), nfcol)
for i in range( nfcol):
mupdf.pdf_array_push_real( col, fcol[i])
mupdf.pdf_dict_put( annot_obj, PDF_NAME('IC'), col)
except Exception as e:
if g_exceptions_verbose: exception_info()
print( f'cannot update annot: {e}', file=sys.stderr)
raise
if (opacity < 0 or opacity >= 1) and not blend_mode:
return True
try:
ap = mupdf.pdf_dict_getl(
mupdf.pdf_annot_obj(annot),
PDF_NAME('AP'),
PDF_NAME('N')
)
if not ap.m_internal:
raise RuntimeError( MSG_BAD_APN)
resources = mupdf.pdf_dict_get( ap, PDF_NAME('Resources'))
if not resources.m_internal:
resources = mupdf.pdf_dict_put_dict( ap, PDF_NAME('Resources'), 2)
alp0 = mupdf.pdf_new_dict( page.doc(), 3)
if opacity >= 0 and opacity < 1:
mupdf.pdf_dict_put_real( alp0, PDF_NAME('CA'), opacity)
mupdf.pdf_dict_put_real( alp0, PDF_NAME('ca'), opacity)
mupdf.pdf_dict_put_real( annot_obj, PDF_NAME('CA'), opacity)
if blend_mode:
mupdf.pdf_dict_put_name( alp0, PDF_NAME('BM'), blend_mode)
mupdf.pdf_dict_put_name( annot_obj, PDF_NAME('BM'), blend_mode)
extg = mupdf.pdf_dict_get( resources, PDF_NAME('ExtGState'))
if not extg.m_internal:
extg = mupdf.pdf_dict_put_dict( resources, PDF_NAME('ExtGState'), 2)
mupdf.pdf_dict_put( extg, PDF_NAME('H'), alp0)
except Exception as e:
if g_exceptions_verbose: exception_info()
print( f'cannot set opacity or blend mode\n: {e}', file=sys.stderr)
raise
return True
@property
def apn_bbox(self):
"""annotation appearance bbox"""
CheckParent(self)
annot = self.this
annot_obj = mupdf.pdf_annot_obj(annot)
ap = mupdf.pdf_dict_getl(annot_obj, PDF_NAME('AP'), PDF_NAME('N'))
if not ap.m_internal:
val = JM_py_from_rect(mupdf.FzRect(mupdf.FzRect.Fixed_INFINITE))
else:
rect = mupdf.pdf_dict_get_rect(ap, PDF_NAME('BBox'))
val = JM_py_from_rect(rect)
val = Rect(val) * self.parent.transformation_matrix
val *= self.parent.derotationMatrix
return val
@property
def apn_matrix(self):
"""annotation appearance matrix"""
try:
CheckParent(self)
annot = self.this
assert isinstance(annot, mupdf.PdfAnnot)
ap = mupdf.pdf_dict_getl(
mupdf.pdf_annot_obj(annot),
mupdf.PDF_ENUM_NAME_AP,
mupdf.PDF_ENUM_NAME_N
)
if not ap.m_internal:
return JM_py_from_matrix(mupdf.FzMatrix())
mat = mupdf.pdf_dict_get_matrix(ap, mupdf.PDF_ENUM_NAME_Matrix)
val = JM_py_from_matrix(mat)
val = Matrix(val)
return val
except Exception:
if g_exceptions_verbose: exception_info()
raise
@property
def blendmode(self):
"""annotation BlendMode"""
CheckParent(self)
annot = self.this
annot_obj = mupdf.pdf_annot_obj(annot)
obj = mupdf.pdf_dict_get(annot_obj, PDF_NAME('BM'))
blend_mode = None
if obj.m_internal:
blend_mode = JM_UnicodeFromStr(mupdf.pdf_to_name(obj))
return blend_mode
obj = mupdf.pdf_dict_getl(
annot_obj,
PDF_NAME('AP'),
PDF_NAME('N'),
PDF_NAME('Resources'),
PDF_NAME('ExtGState'),
)
if mupdf.pdf_is_dict(obj):
n = mupdf.pdf_dict_len(obj)
for i in range(n):
obj1 = mupdf.pdf_dict_get_val(obj, i)
if mupdf.pdf_is_dict(obj1):
m = mupdf.pdf_dict_len(obj1)
for j in range(m):
obj2 = mupdf.pdf_dict_get_key(obj1, j)
if mupdf.pdf_objcmp(obj2, PDF_NAME('BM')) == 0:
blend_mode = JM_UnicodeFromStr(mupdf.pdf_to_name(mupdf.pdf_dict_get_val(obj1, j)))
return blend_mode
return blend_mode
@property
def border(self):
"""Border information."""
CheckParent(self)
atype = self.type[0]
if atype not in (
mupdf.PDF_ANNOT_CIRCLE,
mupdf.PDF_ANNOT_FREE_TEXT,
mupdf.PDF_ANNOT_INK,
mupdf.PDF_ANNOT_LINE,
mupdf.PDF_ANNOT_POLY_LINE,
mupdf.PDF_ANNOT_POLYGON,
mupdf.PDF_ANNOT_SQUARE,
):
return dict()
ao = mupdf.pdf_annot_obj(self.this)
ret = JM_annot_border(ao)
return ret
def clean_contents(self, sanitize=1):
"""Clean appearance contents stream."""
CheckParent(self)
annot = self.this
pdf = mupdf.pdf_get_bound_document(mupdf.pdf_annot_obj(annot))
filter_ = _make_PdfFilterOptions(recurse=1, instance_forms=0, ascii=0, sanitize=sanitize)
mupdf.pdf_filter_annot_contents(pdf, annot, filter_)
@property
def colors(self):
"""Color definitions."""
try:
CheckParent(self)
annot = self.this
assert isinstance(annot, mupdf.PdfAnnot)
return JM_annot_colors(mupdf.pdf_annot_obj(annot))
except Exception:
if g_exceptions_verbose: exception_info()
raise
def delete_responses(self):
"""Delete 'Popup' and responding annotations."""
CheckParent(self)
annot = self.this
annot_obj = mupdf.pdf_annot_obj(annot)
page = mupdf.pdf_annot_page(annot)
while 1:
irt_annot = JM_find_annot_irt(annot)
if not irt_annot.m_internal:
break
mupdf.pdf_delete_annot(page, irt_annot)
mupdf.pdf_dict_del(annot_obj, PDF_NAME('Popup'))
annots = mupdf.pdf_dict_get(page.obj(), PDF_NAME('Annots'))
n = mupdf.pdf_array_len(annots)
found = 0
for i in range(n-1, -1, -1):
o = mupdf.pdf_array_get(annots, i)
p = mupdf.pdf_dict_get(o, PDF_NAME('Parent'))
if not o.m_internal:
continue
if not mupdf.pdf_objcmp(p, annot_obj):
mupdf.pdf_array_delete(annots, i)
found = 1
if found:
mupdf.pdf_dict_put(page.obj(), PDF_NAME('Annots'), annots)
@property
def file_info(self):
"""Attached file information."""
CheckParent(self)
res = dict()
length = -1
size = -1
annot = self.this
annot_obj = mupdf.pdf_annot_obj(annot)
type_ = mupdf.pdf_annot_type(annot)
if type_ != mupdf.PDF_ANNOT_FILE_ATTACHMENT:
raise TypeError( MSG_BAD_ANNOT_TYPE)
stream = mupdf.pdf_dict_getl(
annot_obj,
PDF_NAME('FS'),
PDF_NAME('EF'),
PDF_NAME('F'),
)
if not stream.m_internal:
RAISEPY( "bad PDF: file entry not found", JM_Exc_FileDataError)
fs = mupdf.pdf_dict_get(annot_obj, PDF_NAME('FS'))
o = mupdf.pdf_dict_get(fs, PDF_NAME('UF'))
if o.m_internal:
filename = mupdf.pdf_to_text_string(o)
else:
o = mupdf.pdf_dict_get(fs, PDF_NAME('F'))
if o.m_internal:
filename = mupdf.pdf_to_text_string(o)
o = mupdf.pdf_dict_get(fs, PDF_NAME('Desc'))
if o.m_internal:
desc = mupdf.pdf_to_text_string(o)
o = mupdf.pdf_dict_get(stream, PDF_NAME('Length'))
if o.m_internal:
length = mupdf.pdf_to_int(o)
o = mupdf.pdf_dict_getl(stream, PDF_NAME('Params'), PDF_NAME('Size'))
if o.m_internal:
size = mupdf.pdf_to_int(o)
res[ dictkey_filename] = JM_EscapeStrFromStr(filename)
res[ dictkey_desc] = JM_UnicodeFromStr(desc)
res[ dictkey_length] = length
res[ dictkey_size] = size
return res
@property
def flags(self):
"""Flags field."""
CheckParent(self)
annot = self.this
return mupdf.pdf_annot_flags(annot)
def get_file(self):
"""Retrieve attached file content."""
CheckParent(self)
annot = self.this
annot_obj = mupdf.pdf_annot_obj(annot)
type = mupdf.pdf_annot_type(annot)
if type != mupdf.PDF_ANNOT_FILE_ATTACHMENT:
raise TypeError( MSG_BAD_ANNOT_TYPE)
stream = mupdf.pdf_dict_getl(annot_obj, PDF_NAME('FS'), PDF_NAME('EF'), PDF_NAME('F'))
if not stream.m_internal:
RAISEPY( "bad PDF: file entry not found", JM_Exc_FileDataError)
buf = mupdf.pdf_load_stream(stream)
res = JM_BinFromBuffer(buf)
return res
def get_oc(self):
"""Get annotation optional content reference."""
CheckParent(self)
oc = 0
annot = self.this
annot_obj = mupdf.pdf_annot_obj(annot)
obj = mupdf.pdf_dict_get(annot_obj, PDF_NAME('OC'))
if obj.m_internal:
oc = mupdf.pdf_to_num(obj)
return oc
def get_parent(self):
try:
ret = getattr( self, 'parent')
except AttributeError:
page = mupdf.pdf_annot_page(self.this)
assert isinstance( page, mupdf.PdfPage)
document = Document( page.doc()) if page.m_internal else None
ret = Page(page, document)
self.parent = ret
print(f'No attribute .parent: {type(self)=} {id(self)=}: have set {id(self.parent)=}.')
return ret
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)
ctm = JM_matrix_from_py(matrix)
cs = colorspace
if not cs:
cs = mupdf.fz_device_rgb()
pix = mupdf.pdf_new_pixmap_from_annot( self.this, ctm, cs, mupdf.FzSeparations(0), alpha)
if dpi:
pix.set_dpi(dpi, dpi)
return Pixmap( pix)
def get_sound(self):
"""Retrieve sound stream."""
CheckParent(self)
annot = self.this
annot_obj = mupdf.pdf_annot_obj(annot)
type = mupdf.pdf_annot_type(annot)
sound = mupdf.pdf_dict_get(annot_obj, PDF_NAME('Sound'))
if type != mupdf.PDF_ANNOT_SOUND or not sound.m_internal:
raise TypeError( MSG_BAD_ANNOT_TYPE)
if mupdf.pdf_dict_get(sound, PDF_NAME('F')).m_internal:
RAISEPY( "unsupported sound stream", JM_Exc_FileDataError)
res = dict()
obj = mupdf.pdf_dict_get(sound, PDF_NAME('R'))
if obj.m_internal:
res['rate'] = mupdf.pdf_to_real(obj)
obj = mupdf.pdf_dict_get(sound, PDF_NAME('C'))
if obj.m_internal:
res['channels'] = mupdf.pdf_to_int(obj)
obj = mupdf.pdf_dict_get(sound, PDF_NAME('B'))
if obj.m_internal:
res['bps'] = mupdf.pdf_to_int(obj)
obj = mupdf.pdf_dict_get(sound, PDF_NAME('E'))
if obj.m_internal:
res['encoding'] = mupdf.pdf_to_name(obj)
obj = mupdf.pdf_dict_gets(sound, "CO")
if obj.m_internal:
res['compression'] = mupdf.pdf_to_name(obj)
buf = mupdf.pdf_load_stream(sound)
stream = JM_BinFromBuffer(buf)
res['stream'] = stream
return res
def get_textpage(self, clip=None, flags=0):
"""Make annotation TextPage."""
CheckParent(self)
options = mupdf.FzStextOptions()
options.flags = flags
annot = self.this
stextpage = mupdf.FzStextPage(annot, options)
return TextPage(stextpage)
@property
def has_popup(self):
"""Check if annotation has a Popup."""
CheckParent(self)
annot = self.this
obj = mupdf.pdf_dict_get(mupdf.pdf_annot_obj(annot), PDF_NAME('Popup'))
return True if obj.m_internal else False
@property
def info(self):
"""Various information details."""
CheckParent(self)
annot = self.this
res = dict()
res[dictkey_content] = JM_UnicodeFromStr(mupdf.pdf_annot_contents(annot))
o = mupdf.pdf_dict_get(mupdf.pdf_annot_obj(annot), PDF_NAME('Name'))
res[dictkey_name] = JM_UnicodeFromStr(mupdf.pdf_to_name(o))
o = mupdf.pdf_dict_get(mupdf.pdf_annot_obj(annot), PDF_NAME('T'))
res[dictkey_title] = JM_UnicodeFromStr(mupdf.pdf_to_text_string(o))
o = mupdf.pdf_dict_gets(mupdf.pdf_annot_obj(annot), "CreationDate")
res[dictkey_creationDate] = JM_UnicodeFromStr(mupdf.pdf_to_text_string(o))
o = mupdf.pdf_dict_get(mupdf.pdf_annot_obj(annot), PDF_NAME('M'))
res[dictkey_modDate] = JM_UnicodeFromStr(mupdf.pdf_to_text_string(o))
o = mupdf.pdf_dict_gets(mupdf.pdf_annot_obj(annot), "Subj")
res[dictkey_subject] = mupdf.pdf_to_text_string(o)
o = mupdf.pdf_dict_gets(mupdf.pdf_annot_obj(annot), "NM")
res[dictkey_id] = JM_UnicodeFromStr(mupdf.pdf_to_text_string(o))
return res
def irt_xref():
'''
annotation IRT xref
'''
annot = self.this
annot_obj = mupdf.pdf_annot_obj( annot)
irt = mupdf.pdf_dict_get( annot_obj, PDF_NAME('IRT'))
if not irt.m_internal:
return 0
return mupdf.pdf_to_num( irt)
@property
def is_open(self):
"""Get 'open' status of annotation or its Popup."""
CheckParent(self)
return mupdf.pdf_annot_is_open(self.this)
@property
def language(self):
"""annotation language"""
this_annot = self.this
lang = mupdf.pdf_annot_language(this_annot)
if lang == mupdf.FZ_LANG_UNSET:
return
assert hasattr(mupdf, 'fz_string_from_text_language2')
return mupdf.fz_string_from_text_language2(lang)
@property
def line_ends(self):
"""Line end codes."""
CheckParent(self)
annot = self.this
if not mupdf.pdf_annot_has_line_ending_styles(annot):
return
lstart = mupdf.pdf_annot_line_start_style(annot)
lend = mupdf.pdf_annot_line_end_style(annot)
return lstart, lend
@property
def next(self):
"""Next annotation."""
CheckParent(self)
this_annot = self.this
assert isinstance(this_annot, mupdf.PdfAnnot)
assert this_annot.m_internal
type_ = mupdf.pdf_annot_type(this_annot)
if type_ != mupdf.PDF_ANNOT_WIDGET:
annot = mupdf.pdf_next_annot(this_annot)
else:
annot = mupdf.pdf_next_widget(this_annot)
val = Annot(annot) if annot.m_internal else None
if not val:
return None
val.thisown = True
assert val.parent.this.m_internal_value() == self.parent.this.m_internal_value()
val.parent._annot_refs[id(val)] = val
if val.type[0] == mupdf.PDF_ANNOT_WIDGET:
widget = Widget()
TOOLS._fill_widget(val, widget)
val = widget
return val
@property
def opacity(self):
"""Opacity."""
CheckParent(self)
annot = self.this
opy = -1
ca = mupdf.pdf_dict_get( mupdf.pdf_annot_obj(annot), mupdf.PDF_ENUM_NAME_CA)
if mupdf.pdf_is_number(ca):
opy = mupdf.pdf_to_real(ca)
return opy
@property
def popup_rect(self):
"""annotation 'Popup' rectangle"""
CheckParent(self)
rect = mupdf.FzRect(mupdf.FzRect.Fixed_INFINITE)
annot = self.this
annot_obj = mupdf.pdf_annot_obj( annot)
obj = mupdf.pdf_dict_get( annot_obj, PDF_NAME('Popup'))
if obj.m_internal:
rect = mupdf.pdf_dict_get_rect(obj, PDF_NAME('Rect'))
val = JM_py_from_rect(rect)
val = Rect(val) * self.parent.transformation_matrix
val *= self.parent.derotation_matrix
return val
@property
def popup_xref(self):
"""annotation 'Popup' xref"""
CheckParent(self)
xref = 0
annot = self.this
annot_obj = mupdf.pdf_annot_obj(annot)
obj = mupdf.pdf_dict_get(annot_obj, PDF_NAME('Popup'))
if obj.m_internal:
xref = mupdf.pdf_to_num(obj)
return xref
@property
def rect(self):
"""annotation rectangle"""
if g_use_extra:
val = extra.Annot_rect3( self.this)
else:
val = mupdf.pdf_bound_annot(self.this)
val = Rect(val)
p = self.get_parent()
val *= p.derotation_matrix
return val
@property
def rect_delta(self):
'''
annotation delta values to rectangle
'''
annot_obj = mupdf.pdf_annot_obj(self.this)
arr = mupdf.pdf_dict_get( annot_obj, PDF_NAME('RD'))
if mupdf.pdf_array_len( arr) == 4:
return (
mupdf.pdf_to_real( mupdf.pdf_array_get( arr, 0)),
mupdf.pdf_to_real( mupdf.pdf_array_get( arr, 1)),
-mupdf.pdf_to_real( mupdf.pdf_array_get( arr, 2)),
-mupdf.pdf_to_real( mupdf.pdf_array_get( arr, 3)),
)
@property
def rotation(self):
"""annotation rotation"""
CheckParent(self)
annot = self.this
rotation = mupdf.pdf_dict_get( mupdf.pdf_annot_obj(annot), mupdf.PDF_ENUM_NAME_Rotate)
if not rotation.m_internal:
return -1
return mupdf.pdf_to_int( rotation)
def set_apn_bbox(self, bbox):
"""
Set annotation appearance bbox.
"""
CheckParent(self)
page = self.parent
rot = page.rotationMatrix
mat = page.transformation_matrix
bbox *= rot * ~mat
annot = self.this
annot_obj = mupdf.pdf_annot_obj(annot)
ap = mupdf.pdf_dict_getl(annot_obj, PDF_NAME('AP'), PDF_NAME('N'))
if not ap.m_internal:
raise RuntimeError( MSG_BAD_APN)
rect = JM_rect_from_py(bbox)
mupdf.pdf_dict_put_rect(ap, PDF_NAME('BBox'), rect)
def set_apn_matrix(self, matrix):
"""Set annotation appearance matrix."""
CheckParent(self)
annot = self.this
annot_obj = mupdf.pdf_annot_obj(annot)
ap = mupdf.pdf_dict_getl(annot_obj, PDF_NAME('AP'), PDF_NAME('N'))
if not ap.m_internal:
raise RuntimeError( MSG_BAD_APN)
mat = JM_matrix_from_py(matrix)
mupdf.pdf_dict_put_matrix(ap, PDF_NAME('Matrix'), mat)
def set_blendmode(self, blend_mode):
"""Set annotation BlendMode."""
CheckParent(self)
annot = self.this
annot_obj = mupdf.pdf_annot_obj(annot)
mupdf.pdf_dict_put_name(annot_obj, PDF_NAME('BM'), blend_mode)
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]
if atype not in (
mupdf.PDF_ANNOT_CIRCLE,
mupdf.PDF_ANNOT_FREE_TEXT,
mupdf.PDF_ANNOT_INK,
mupdf.PDF_ANNOT_LINE,
mupdf.PDF_ANNOT_POLY_LINE,
mupdf.PDF_ANNOT_POLYGON,
mupdf.PDF_ANNOT_SQUARE,
):
print(f"Cannot set border for '{atname}'.")
return None
if atype not in (
mupdf.PDF_ANNOT_CIRCLE,
mupdf.PDF_ANNOT_FREE_TEXT,
mupdf.PDF_ANNOT_POLYGON,
mupdf.PDF_ANNOT_SQUARE,
):
if clouds > 0:
print(f"Cannot set cloudy border for '{atname}'.")
clouds = -1
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"] is None:
border["width"] = -1
if border["clouds"] is None:
border["clouds"] = -1
if hasattr(border["dashes"], "__getitem__"):
border["dashes"] = tuple(border["dashes"])
for item in border["dashes"]:
if not isinstance(item, int):
border["dashes"] = None
break
annot = self.this
annot_obj = mupdf.pdf_annot_obj( annot)
pdf = mupdf.pdf_get_bound_document( annot_obj)
return JM_annot_set_border( border, pdf, annot_obj)
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 = (mupdf.PDF_ANNOT_CIRCLE, mupdf.PDF_ANNOT_SQUARE, mupdf.PDF_ANNOT_LINE, mupdf.PDF_ANNOT_POLY_LINE, mupdf.PDF_ANNOT_POLYGON,
mupdf.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)
def set_flags(self, flags):
"""Set annotation flags."""
CheckParent(self)
annot = self.this
mupdf.pdf_set_annot_flags(annot, flags)
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:
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
annot = self.this
is_markup = mupdf.pdf_annot_has_author(annot)
if content:
mupdf.pdf_set_annot_contents(annot, content)
if is_markup:
if title:
mupdf.pdf_set_annot_author(annot, title)
if creationDate:
mupdf.pdf_dict_put_text_string(mupdf.pdf_annot_obj(annot), PDF_NAME('CreationDate'), creationDate)
if modDate:
mupdf.pdf_dict_put_text_string(mupdf.pdf_annot_obj(annot), PDF_NAME('M'), modDate)
if subject:
mupdf.pdf_dict_puts(mupdf.pdf_annot_obj(annot), "Subj", mupdf.pdf_new_text_string(subject))
def set_irt_xref( xref):
'''
Set annotation IRT xref
'''
annot = self.this
annot_obj = mupdf.pdf_annot_obj( annot)
page = mupdf.pdf_annot_page( annot)
if xref < 1 or xref >= mupdf.pdf_xref_len( page.doc()):
raise ValueError( MSG_BAD_XREF)
irt = mupdf.pdf_new_indirect( page.doc(), xref, 0)
subt = mupdf.pdf_dict_get( irt, PDF_NAME('Subtype'))
irt_subt = mupdf.pdf_annot_type_from_string( mupdf.pdf_to_name( subt))
if irt_subt < 0:
raise ValueError( MSG_IS_NO_ANNOT)
mupdf.pdf_dict_put( annot_obj, PDF_NAME('IRT'), irt)
def set_language(self, language=None):
"""Set annotation language."""
CheckParent(self)
this_annot = self.this
if not language:
lang = mupdf.FZ_LANG_UNSET
else:
lang = mupdf.fz_text_language_from_string(language)
mupdf.pdf_set_annot_language(this_annot, lang)
def set_line_ends(self, start, end):
"""Set line end codes."""
CheckParent(self)
annot = self.this
if mupdf.pdf_annot_has_line_ending_styles(annot):
mupdf.pdf_set_annot_line_ending_styles(annot, start, end)
else:
JM_Warning("bad annot type for line ends")
def set_name(self, name):
"""Set /Name (icon) of annotation."""
CheckParent(self)
annot = self.this
annot_obj = mupdf.pdf_annot_obj(annot)
mupdf.pdf_dict_put_name(annot_obj, PDF_NAME('Name'), name)
def set_oc(self, oc=0):
"""Set / remove annotation OC xref."""
CheckParent(self)
annot = self.this
annot_obj = mupdf.pdf_annot_obj(annot)
if not oc:
mupdf.pdf_dict_del(annot_obj, PDF_NAME('OC'))
else:
JM_add_oc_object(mupdf.pdf_get_bound_document(annot_obj), annot_obj, oc)
def set_opacity(self, opacity):
"""Set opacity."""
CheckParent(self)
annot = self.this
if not _INRANGE(opacity, 0.0, 1.0):
mupdf.pdf_set_annot_opacity(annot, 1)
return
mupdf.pdf_set_annot_opacity(annot, opacity)
if opacity < 1.0:
page = mupdf.pdf_annot_page(annot)
page.transparency = 1
def set_open(self, is_open):
"""Set 'open' status of annotation or its Popup."""
CheckParent(self)
annot = self.this
mupdf.pdf_set_annot_is_open(annot, is_open)
def set_popup(self, rect):
'''
Create annotation 'Popup' or update rectangle.
'''
CheckParent(self)
annot = self.this
pdfpage = mupdf.pdf_annot_page( annot)
rot = JM_rotate_page_matrix(pdfpage)
r = mupdf.fz_transform_rect(JM_rect_from_py(rect), rot)
mupdf.pdf_set_annot_popup(annot, r)
def set_rect(self, rect):
"""Set annotation rectangle."""
CheckParent(self)
annot = self.this
pdfpage = mupdf.pdf_annot_page(annot)
rot = JM_rotate_page_matrix(pdfpage)
r = mupdf.fz_transform_rect(JM_rect_from_py(rect), rot)
if mupdf.fz_is_empty_rect(r) or mupdf.fz_is_infinite_rect(r):
raise ValueError( MSG_BAD_RECT)
try:
mupdf.pdf_set_annot_rect(annot, r)
except Exception as e:
print(f'cannot set rect: {e}')
return False
def set_rotation(self, rotate=0):
"""Set annotation rotation."""
CheckParent(self)
annot = self.this
type = mupdf.pdf_annot_type(annot)
if type not in (
mupdf.PDF_ANNOT_CARET,
mupdf.PDF_ANNOT_CIRCLE,
mupdf.PDF_ANNOT_FREE_TEXT,
mupdf.PDF_ANNOT_FILE_ATTACHMENT,
mupdf.PDF_ANNOT_INK,
mupdf.PDF_ANNOT_LINE,
mupdf.PDF_ANNOT_POLY_LINE,
mupdf.PDF_ANNOT_POLYGON,
mupdf.PDF_ANNOT_SQUARE,
mupdf.PDF_ANNOT_STAMP,
mupdf.PDF_ANNOT_TEXT,
):
return
rot = rotate
while rot < 0:
rot += 360
while rot >= 360:
rot -= 360
if type == mupdf.PDF_ANNOT_FREE_TEXT and rot % 90 != 0:
rot = 0
annot_obj = mupdf.pdf_annot_obj(annot)
mupdf.pdf_dict_put_int(annot_obj, PDF_NAME('Rotate'), rot)
@property
def type(self):
"""annotation type"""
CheckParent(self)
if not self.this.m_internal:
return 'null'
type_ = mupdf.pdf_annot_type(self.this)
c = mupdf.pdf_string_from_annot_type(type_)
o = mupdf.pdf_dict_gets( mupdf.pdf_annot_obj(self.this), 'IT')
if not o.m_internal or mupdf.pdf_is_name(o):
return (type_, c)
it = mupdf.pdf_to_name(o)
return (type_, c, it)
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.
"""
Annot.update_timing_test()
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]
dt = self.border.get("dashes", None)
bwidth = self.border.get("width", -1)
stroke = self.colors["stroke"]
if fill_color is not None:
fill = fill_color
else:
fill = self.colors["fill"]
rect = None
apnmat = self.apn_matrix
if rotate != -1:
while rotate < 0:
rotate += 360
while rotate >= 360:
rotate -= 360
if annot_type == mupdf.PDF_ANNOT_FREE_TEXT and rotate % 90 != 0:
rotate = 0
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"
else:
opa_code = ""
if annot_type == mupdf.PDF_ANNOT_FREE_TEXT:
CheckColor(border_color)
CheckColor(text_color)
CheckColor(fill_color)
tcol, fname, fsize = TOOLS._parse_da(self)
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)
val = self._update_appearance(
opacity=opacity,
blend_mode=blend_mode,
fill_color=fill,
rotate=rotate,
)
if val is False:
raise RuntimeError("Error updating annotation.")
bfill = color_string(fill, "f")
bstroke = color_string(stroke, "c")
p_ctm = self.get_parent().transformation_matrix
imat = ~p_ctm
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
ap = self._getAP()
ap_tab = ap.splitlines()
ap_updated = False
if annot_type == mupdf.PDF_ANNOT_REDACT:
if cross_out:
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 == mupdf.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 is not None:
ap = bwidth + fill_string + stroke_string + re + b"\n" + ope + b"\n" + ap
if dashes is not None:
ap = dashes + b"\n" + ap
dashes = None
ap_updated = True
if annot_type in (mupdf.PDF_ANNOT_POLYGON, mupdf.PDF_ANNOT_POLY_LINE):
ap = b"\n".join(ap_tab[:-1]) + b"\n"
ap_updated = True
if bfill != b"":
if annot_type == mupdf.PDF_ANNOT_POLYGON:
ap = ap + bfill + b"b"
elif annot_type == mupdf.PDF_ANNOT_POLY_LINE:
ap = ap + b"S"
else:
if annot_type == mupdf.PDF_ANNOT_POLYGON:
ap = ap + b"s"
elif annot_type == mupdf.PDF_ANNOT_POLY_LINE:
ap = ap + b"S"
if dashes is not None:
ap = dashes + ap
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"
if line_end_le + line_end_ri > 0 and annot_type in (mupdf.PDF_ANNOT_POLYGON, mupdf.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:
self.set_rect(rect)
self._setAP(ap, rect=1)
else:
self._setAP(ap, rect=0)
if annot_type not in (
mupdf.PDF_ANNOT_CARET,
mupdf.PDF_ANNOT_CIRCLE,
mupdf.PDF_ANNOT_FILE_ATTACHMENT,
mupdf.PDF_ANNOT_INK,
mupdf.PDF_ANNOT_LINE,
mupdf.PDF_ANNOT_POLY_LINE,
mupdf.PDF_ANNOT_POLYGON,
mupdf.PDF_ANNOT_SQUARE,
mupdf.PDF_ANNOT_STAMP,
mupdf.PDF_ANNOT_TEXT,
):
return
rot = self.rotation
if rot == -1:
return
M = (self.rect.tl + self.rect.br) / 2
if rot == 0:
if abs(apnmat - Matrix(1, 1)) < 1e-5:
return
quad = self.rect.morph(M, ~apnmat)
self.setRect(quad.rect)
self.set_apn_matrix(Matrix(1, 1))
return
mat = Matrix(rot)
quad = self.rect.morph(M, mat)
self.set_rect(quad.rect)
self.set_apn_matrix(apnmat * mat)
def update_file(self, buffer_=None, filename=None, ufilename=None, desc=None):
"""Update attached file."""
CheckParent(self)
annot = self.this
annot_obj = mupdf.pdf_annot_obj(annot)
pdf = mupdf.pdf_get_bound_document(annot_obj)
type = mupdf.pdf_annot_type(annot)
if type != mupdf.PDF_ANNOT_FILE_ATTACHMENT:
raise TypeError( MSG_BAD_ANNOT_TYPE)
stream = mupdf.pdf_dict_getl(annot_obj, PDF_NAME('FS'), PDF_NAME('EF'), PDF_NAME('F'))
if not stream.m_internal:
RAISEPY( "bad PDF: no /EF object", JM_Exc_FileDataError)
fs = mupdf.pdf_dict_get(annot_obj, PDF_NAME('FS'))
res = JM_BufferFromBytes(buffer_)
if buffer_ and not res.m_internal:
raise ValueError( MSG_BAD_BUFFER)
if res:
JM_update_stream(pdf, stream, res, 1)
len, _ = mupdf.fz_buffer_storage(res)
l = mupdf.pdf_new_int(len)
mupdf.pdf_dict_put(stream, PDF_NAME('DL'), l)
mupdf.pdf_dict_putl(stream, l, PDF_NAME('Params'), PDF_NAME('Size'))
if filename:
mupdf.pdf_dict_put_text_string(stream, PDF_NAME('F'), filename)
mupdf.pdf_dict_put_text_string(fs, PDF_NAME('F'), filename)
mupdf.pdf_dict_put_text_string(stream, PDF_NAME('UF'), filename)
mupdf.pdf_dict_put_text_string(fs, PDF_NAME('UF'), filename)
mupdf.pdf_dict_put_text_string(annot_obj, PDF_NAME('Contents'), filename)
if ufilename:
mupdf.pdf_dict_put_text_string(stream, PDF_NAME('UF'), ufilename)
mupdf.pdf_dict_put_text_string(fs, PDF_NAME('UF'), ufilename)
if desc:
mupdf.pdf_dict_put_text_string(stream, PDF_NAME('Desc'), desc)
mupdf.pdf_dict_put_text_string(fs, PDF_NAME('Desc'), desc)
@staticmethod
def update_timing_test():
total = 0
for i in range( 30*1000):
total += i
return total
@property
def vertices(self):
"""annotation vertex points"""
CheckParent(self)
annot = self.this
assert isinstance(annot, mupdf.PdfAnnot)
page_ctm = mupdf.FzMatrix()
dummy = mupdf.FzRect(0)
mupdf.pdf_page_transform(mupdf.pdf_annot_page(annot), dummy, page_ctm)
derot = JM_derotate_page_matrix(mupdf.pdf_annot_page(annot))
page_ctm = mupdf.fz_concat(page_ctm, derot)
o = mupdf.pdf_dict_get(mupdf.pdf_annot_obj(annot), PDF_NAME('Vertices'))
if not o.m_internal: o = mupdf.pdf_dict_get(mupdf.pdf_annot_obj(annot), PDF_NAME('L'))
if not o.m_internal: o = mupdf.pdf_dict_get(mupdf.pdf_annot_obj(annot), PDF_NAME('QuadPoints'))
if not o.m_internal: o = mupdf.pdf_dict_gets(mupdf.pdf_annot_obj(annot), 'CL')
if o.m_internal:
res = []
for i in range(0, mupdf.pdf_array_len(o), 2):
x = mupdf.pdf_to_real(mupdf.pdf_array_get(o, i))
y = mupdf.pdf_to_real(mupdf.pdf_array_get(o, i+1))
point = mupdf.FzPoint(x, y)
point = mupdf.fz_transform_point(point, page_ctm)
res.append( (point.x, point.y))
return res
else:
res = []
for i in range(mupdf.pdf_array_len(o)):
res1 = []
o1 = mupdf.pdf_array_get(o, i)
for j in range(0, mupdf.pdf_array_len(o1), 2):
x = mupdf.pdf_to_real(mupdf.pdf_array_get(o1, j))
y = mupdf.pdf_to_real(mupdf.pdf_array_get(o1, j+1))
point = mupdf.FzPoint(x, y)
point = mupdf.fz_transform_point(point, page_ctm)
res1.append( (point.x, point.y))
res.append(res1)
return res
@property
def xref(self):
"""annotation xref number"""
CheckParent(self)
annot = self.this
return mupdf.pdf_to_num(mupdf.pdf_annot_obj(annot))
class Archive:
def __init__( self, *args):
'''
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
'''
self._subarchives = list()
self.this = mupdf.fz_new_multi_archive()
if args:
self.add( *args)
def __repr__( self):
return f'Archive, sub-archives: {len(self._subarchives)}'
def _add_arch( subarch, path=None):
mupdf.fz_mount_multi_archive( self.this, subarch, path)
def _add_dir( self, folder, path=None):
sub = mupdf.fz_open_directory( folder)
mupdf.fz_mount_multi_archive( self.this, sub, path)
def _add_treeitem( self, memory, name, path=None):
drop_sub = False
buff = JM_BufferFromBytes( memory)
sub = JM_last_tree( self.this, path)
if not sub:
sub = mupdf.fz_new_tree_archive( None)
drop_sub = True
mupdf.fz_tree_archive_add_buffer( sub, name, buff)
if drop_sub:
mupdf.fz_mount_multi_archive( self.this, sub, path)
def _add_ziptarfile( self, filepath, type_, path=None):
if type_ == 1:
sub = mupdf.fz_open_zip_archive( filepath)
else:
sub = mupdf.fz_open_tar_archive( filepath)
mupdf.fz_mount_multi_archive( self.this, sub, path)
def _add_ziptarmemory( self, memory, type_, path=None):
buff = JM_BufferFromBytes( memory)
stream = mupdf.fz_open_buffer( buff)
if type_==1:
sub = mupdf.fz_open_zip_archive_with_stream( stream)
else:
sub = mupdf.fz_open_tar_archive_with_stream( stream)
mupdf.fz_mount_multi_archive( self.this, sub, 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.
'''
def bin_ok(x):
return 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")
if len(content) == 2:
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()
self._add_treeitem(ff, name, path=mount)
entries = [name]
return make_subarch()
for item in content:
self.add(item, path)
@property
def entry_list( self):
'''
List of sub archives.
'''
return self._subarchives
def has_entry( self, name):
return mupdf.fz_has_archive_entry( self.this, name)
def read_entry( self, name):
buff = mupdf.fz_read_archive_entry( self.this, name)
return JM_BinFromBuffer( buff)
class Xml:
def __enter__(self):
return self
def __exit__(self, *args):
pass
def __init__( self, rhs):
if isinstance( rhs, mupdf.FzXml):
self.this = rhs
elif isinstance( str):
buff = mupdf.fz_new_buffer_from_copied_data( rhs)
self.this = mupdf.fz_parse_xml_from_html5( buff)
else:
assert 0, f'Unsupported type for rhs: {type(rhs)}'
def _get_node_tree( self):
def show_node(node, items, shift):
while node is not 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 add_bullet_list(self):
"""Add bulleted list ("ul" tag)"""
child = self.create_element("ul")
self.append_child(child)
return child
def add_class(self, text):
"""Set some class via CSS. Replaces complete class spec."""
cls = self.get_attribute_value("class")
if cls is not None and text in cls:
return self
self.remove_attribute("class")
if cls is None:
cls = text
else:
cls += " " + text
self.set_attribute("class", cls)
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 is 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 add_description_list(self):
"""Add description list ("dl" tag)"""
child = self.create_element("dl")
self.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_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)
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_horizontal_line(self):
"""Add horizontal line ("hr" tag)"""
child = self.create_element("hr")
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 is not None:
child.set_attribute("width", f"{width}")
if height is not None:
child.set_attribute("height", f"{height}")
if imgfloat is not None:
child.set_attribute("style", f"float: {imgfloat}")
if align is not None:
child.set_attribute("align", f"{align}")
child.set_attribute("src", f"{name}")
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 is None:
prev = self
prev.append_child(child)
return self
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_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 is not None:
child.set_attribute("type", numtype)
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_span(self):
child = self.create_element("span")
self.append_child(child)
return child
def add_style(self, text):
"""Set some style via CSS style. Replaces complete style spec."""
style = self.get_attribute_value("style")
if style is not None and text in style:
return self
self.remove_attribute("style")
if style is None:
style = text
else:
style += ";" + text
self.set_attribute("style", style)
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 is None:
prev = self
prev.append_child(child)
return self
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 is None:
prev = self
prev.append_child(child)
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 is 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 append_child( self, child):
mupdf.fz_dom_append_child( self.this, child.this)
def append_styled_span(self, style):
span = self.create_element("span")
span.add_style(style)
prev = self.span_bottom()
if prev is None:
prev = self
prev.append_child(span)
return prev
def bodytag( self):
return Xml( mupdf.fz_dom_body( self.this))
def clone( self):
ret = mupdf.fz_dom_clone( self.this)
return Xml( ret)
@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 create_element( self, tag):
return Xml( mupdf.fz_dom_create_element( self.this, tag))
def create_text_node( self, text):
return Xml( mupdf.fz_dom_create_text_node( self.this, text))
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"))
def find( self, tag, att, match):
ret = mupdf.fz_dom_find( self.this, tag, att, match)
if ret.m_internal:
return Xml( ret)
def find_next( self, tag, att, match):
ret = mupdf.fz_dom_find_next( self.this, tag, att, match)
if ret.m_internal:
return Xml( ret)
@property
def first_child( self):
if mupdf.fz_xml_text( self.this):
return
ret = mupdf.fz_dom_first_child( self)
if ret.m_internal:
return Xml( ret)
def get_attribute_value( self, key):
assert key
return mupdf.fz_dom_attribute( self.this, key)
def get_attributes( self):
if mupdf.fz_xml_text( self.this):
return
result = dict()
i = 0
while 1:
val, key = mupdf.fz_dom_get_attribute( self.this, i)
if not val or not key:
break
result[ key] = val
i += 1
return result
def insert_after( self, node):
mupdf.fz_dom_insert_after( self.this, node.this)
def insert_before( self, node):
mupdf.fz_dom_insert_before( self.this, node.this)
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
@property
def is_text(self):
"""Check if this is a text node."""
return self.text is not None
@property
def last_child(self):
"""Return last child node."""
child = self.first_child
if child is None:
return None
while True:
next = child.next
if not next:
return child
child = next
@property
def next( self):
ret = mupdf.fz_dom_next( self.this)
if ret.m_internal:
return Xml( ret)
@property
def parent( self):
ret = mupdf.fz_dom_parent( self.this)
if ret.m_internal:
return Xml( ret)
@property
def previous( self):
ret = mupdf.fz_dom_previous( self.this)
if ret.m_internal:
return Xml( ret)
def remove( self):
mupdf.fz_dom_remove( self.this)
def remove_attribute( self, key):
assert key
mupdf.fz_dom_remove_attribute( self.this, key)
@property
def root( self):
return Xml( mupdf.fz_xml_root( self.this))
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=}")
text = text % t
self.add_style(text)
return self
def set_attribute( self, key, value):
assert key
mupdf.fz_dom_add_attribute( self.this, key, value)
def set_bgcolor(self, color):
"""Set background color via CSS style"""
text = f"background-color: %s" % self.color_text(color)
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_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_font(self, font):
"""Set font-family name via CSS style"""
text = "font-family: %s" % font
self.append_styled_span(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_id(self, unique):
"""Set a unique id."""
root = self.root
if root.find(None, "id", unique):
raise ValueError(f"id '{unique}' already exists")
self.set_attribute("id", unique)
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_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_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_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_margins(self, val):
"""Set margin values via CSS style"""
text = "margins: %s" % val
self.append_styled_span(text)
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_pagebreak_after(self):
"""Insert a page break after this node."""
text = "page-break-after: always"
self.add_style(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_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_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_underline(self, val="underline"):
text = "text-decoration: %s" % val
self.append_styled_span(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 span_bottom(self):
"""Find deepest level in stacked spans."""
sys.stdout = sys.stderr
parent = self
child = self.last_child
if child is None:
return None
while child.is_text:
child = child.previous
if child is None:
break
if child is None or child.tagname != "span":
return None
while True:
if child is 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
@property
def tagname( self):
return mupdf.fz_xml_tag( self.this)
@property
def text( self):
return mupdf.fz_xml_text( self.this)
add_var = add_code
add_samp = add_code
add_kbd = add_code
class Colorspace:
def __init__(self, type_):
"""Supported are GRAY, RGB and CMYK."""
if type_ == CS_GRAY:
self.this = mupdf.FzColorspace(mupdf.FzColorspace.Fixed_GRAY)
elif type_ == CS_CMYK:
self.this = mupdf.FzColorspace(mupdf.FzColorspace.Fixed_CMYK)
elif type_ == CS_RGB:
self.this = mupdf.FzColorspace(mupdf.FzColorspace.Fixed_RGB)
else:
self.this = mupdf.FzColorspace(mupdf.FzColorspace.Fixed_RGB)
def __repr__(self):
x = ("", "GRAY", "", "RGB", "CMYK")[self.n]
return "Colorspace(CS_%s) - %s" % (x, self.name)
def _name(self):
return mupdf.fz_colorspace_name(self.this)
@property
def n(self):
"""Size of one pixel."""
return mupdf.fz_colorspace_n(self.this)
@property
def name(self):
"""Name of the Colorspace."""
return self._name()
class DeviceWrapper:
def __init__(self, *args):
if args_match( args, mupdf.FzDevice):
device, = args
self.this = device
elif args_match( args, Pixmap, None):
pm, clip = args
bbox = JM_irect_from_py( clip)
if mupdf.fz_is_infinite_irect( bbox):
self.this = mupdf.fz_new_draw_device( mupdf.FzMatrix(), pm)
else:
self.this = mupdf.fz_new_draw_device_with_bbox( mupdf.FzMatrix(), pm, bbox)
elif args_match( args, mupdf.FzDisplayList):
dl, = args
self.this = mupdf.fz_new_list_device( dl)
elif args_match( args, mupdf.FzStextPage, None):
tp, flags = args
opts = mupdf.FzStextOptions( flags)
self.this = mupdf.fz_new_stext_device( tp, opts)
else:
raise Exception( f'Unrecognised args for DeviceWrapper: {args!r}')
class DisplayList:
def __del__(self):
if not type(self) is DisplayList: return
self.thisown = False
def __init__(self, *args):
if len(args) == 1 and isinstance(args[0], mupdf.FzRect):
self.this = mupdf.FzDisplayList(args[0])
elif len(args) == 1 and isinstance(args[0], mupdf.FzDisplayList):
self.this = args[0]
else:
assert 0, f'Unrecognised {args=}'
def get_pixmap(self, matrix=None, colorspace=None, alpha=0, clip=None):
if not colorspace:
colorspace = mupdf.FzColorspace(mupdf.FzColorspace.Fixed_RGB)
val = JM_pixmap_from_display_list(self.this, matrix, colorspace, alpha, clip, None)
val.thisown = True
return val
def get_textpage(self, flags=3):
stext_options = mupdf.FzStextOptions()
stext_options.flags = flags
val = mupdf.fz_new_stext_page_from_display_list( self.this, stext_options)
val.thisown = True
return val
@property
def rect(self):
val = JM_py_from_rect(mupdf.fz_bound_display_list(self.this))
val = Rect(val)
return val
def run(self, dw, m, area):
mupdf.fz_run_display_list(
self.this,
dw.device,
JM_matrix_from_py(m),
JM_rect_from_py(area),
mupdf.FzCookie(),
)
if g_use_extra:
extra_FzDocument_insert_pdf = extra.FzDocument_insert_pdf
class Document:
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 (0
or not isinstance(chapter, int)
or chapter < 0
or chapter >= self.chapter_count
):
return False
if (0
or not isinstance(pno, int)
or pno < 0
or pno >= self.chapter_page_count(chapter)
):
return False
return True
def __delitem__(self, i)->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 __enter__(self):
return self
def __exit__(self, *args):
self.close()
def __getitem__(self, i: int =0):
if i not in self:
raise IndexError(f"page {i} not in document")
return self.load_page(i)
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 or pathlib.Path, must have supported
file extension.
open(type, buffer) - type: valid extension, buffer: bytes object.
open(stream=buffer, filetype=type) - keyword version of previous.
open(filename, fileype=type) - filename with unrecognized extension.
rect, width, height, fontsize: layout reflowable document
on open (e.g. EPUB). Ignored if n/a.
"""
global JM_mupdf_show_errors
JM_mupdf_show_errors_old = JM_mupdf_show_errors
JM_mupdf_show_errors = 0
try:
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 isinstance(filename, mupdf.PdfDocument):
pdf_document = filename
self.this = pdf_document
self.this_is_pdf = True
return
if not filename or type(filename) is str:
pass
elif hasattr(filename, "absolute"):
filename = str(filename)
elif hasattr(filename, "name"):
filename = filename.name
else:
raise TypeError("bad filename")
if stream is not 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:
raise TypeError("bad type: 'stream'")
stream = self.stream
if not (filename or filetype):
filename = 'pdf'
else:
self.stream = None
if filename and self.stream is None:
from_file = True
self.name = filename
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)
if g_use_extra:
try:
self.this = extra.Document_init( filename, stream, filetype, rect, width, height, fontsize)
except Exception as e:
e_str = str(e)
if str(e) == MSG_BAD_FILETYPE:
raise ValueError( e_str) from e
else:
raise FileDataError( MSG_BAD_DOCUMENT) from e
else:
w = width
h = height
r = JM_rect_from_py(rect)
if not mupdf.fz_is_infinite_rect(r):
w = r.x1 - r.x0
h = r.y1 - r.y0
if stream:
assert isinstance(stream, bytes)
c = stream
if mupdf_cppyy:
buffer_ = mupdf.fz_new_buffer_from_copied_data( c)
data = mupdf.fz_open_buffer( buffer_)
else:
data = mupdf.fz_open_memory(mupdf.python_buffer_data(c), len(c))
magic = filename
if not magic:
magic = filetype
doc = mupdf.fz_open_document_with_stream(magic, data)
else:
if filename:
if not filetype:
try:
doc = mupdf.fz_open_document(filename)
except Exception as e:
raise EmptyFileError( 'cannot open empty document') from e
else:
handler = mupdf.ll_fz_recognize_document(filetype)
if handler:
if handler.open:
try:
doc = mupdf.ll_fz_document_open_fn_call( handler.open, filename)
except Exception as e:
raise FileDataError( MSG_BAD_DOCUMENT) from e
doc = mupdf.FzDocument( doc)
elif handler.open_with_stream:
data = mupdf.fz_open_file( filename)
doc = mupdf.fz_document_open_with_stream_fn_call( handler.open_with_stream, data)
else:
raise ValueError( MSG_BAD_FILETYPE)
else:
pdf = mupdf.PdfDocument()
doc = mupdf.FzDocument(pdf)
if w > 0 and h > 0:
mupdf.fz_layout_document(doc, w, h, fontsize)
elif mupdf.fz_is_document_reflowable(doc):
mupdf.fz_layout_document(doc, 400, 600, 11)
this = doc
self.this = this
self.thisown = True
if self.thisown:
self._graft_id = TOOLS.gen_id()
if self.needs_pass:
self.isEncrypted = True
self.is_encrypted = True
else:
self.init_doc()
if filename and filename.lower().endswith("svg") or filetype and "svg" in filetype.lower():
try:
_ = self.convert_to_pdf()
except Exception as e:
raise FileDataError("cannot open broken document") from e
if g_use_extra:
self.this_is_pdf = isinstance( self.this, mupdf.PdfDocument)
if self.this_is_pdf:
self.page_count2 = extra.page_count_pdf
else:
self.page_count2 = extra.page_count_fz
finally:
JM_mupdf_show_errors = JM_mupdf_show_errors_old
def __len__(self) -> int:
return self.page_count
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 _addFormFont(self, name, font):
"""Add new form font."""
if self.is_closed or self.isEncrypted:
raise ValueError("document closed or encrypted")
pdf = _as_pdf_document(self)
if not pdf:
return
fonts = mupdf.pdf_dict_getl(
mupdf.pdf_trailer( pdf),
PDF_NAME('Root'),
PDF_NAME('AcroForm'),
PDF_NAME('DR'),
PDF_NAME('Font'),
)
if not fonts.m_internal or not mupdf.pdf_is_dict( fonts):
raise RuntimeError( "PDF has no form fonts yet")
k = mupdf.pdf_new_name( name)
v = JM_pdf_obj_from_str( pdf, font)
mupdf.pdf_dict_put( fonts, k, v)
def _delToC(self):
"""Delete the TOC."""
if self.is_closed or self.isEncrypted:
raise ValueError("document closed or encrypted")
xrefs = []
pdf = _as_pdf_document(self)
if not pdf:
return xrefs
root = mupdf.pdf_dict_get(mupdf.pdf_trailer(pdf), PDF_NAME('Root'))
olroot = mupdf.pdf_dict_get(root, PDF_NAME('Outlines'))
if not olroot.m_internal:
return xrefs
first = mupdf.pdf_dict_get(olroot, PDF_NAME('First'))
xrefs = JM_outline_xrefs(first, xrefs)
xref_count = len(xrefs)
olroot_xref = mupdf.pdf_to_num(olroot)
mupdf.pdf_delete_object(pdf, olroot_xref)
mupdf.pdf_dict_del(root, PDF_NAME('Outlines'))
for i in range(xref_count):
_, xref = JM_INT_ITEM(xrefs, i)
mupdf.pdf_delete_object(pdf, xref)
xrefs.append(olroot_xref)
val = xrefs
self.init_doc()
return val
def _delete_page(self, pno):
pdf = _as_pdf_document(self)
mupdf.pdf_delete_page( pdf, pno)
if pdf.m_internal.rev_page_map:
mupdf.ll_pdf_drop_page_tree( pdf.m_internal)
def _deleteObject(self, xref):
"""Delete object."""
if self.is_closed:
raise ValueError("document closed")
pdf = _as_pdf_document(self)
ASSERT_PDF(pdf)
if not _INRANGE(xref, 1, mupdf.pdf_xref_len(pdf)-1):
raise ValueError( MSG_BAD_XREF)
mupdf.pdf_delete_object(pdf, xref)
def _embeddedFileGet(self, idx):
pdf = _as_pdf_document(self)
names = mupdf.pdf_dict_getl(
mupdf.pdf_trailer(pdf),
PDF_NAME('Root'),
PDF_NAME('Names'),
PDF_NAME('EmbeddedFiles'),
PDF_NAME('Names'),
)
entry = mupdf.pdf_array_get(names, 2*idx+1)
filespec = mupdf.pdf_dict_getl(entry, PDF_NAME('EF'), PDF_NAME('F'))
buf = mupdf.pdf_load_stream(filespec)
cont = JM_BinFromBuffer(buf)
return cont
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_add(self, name, buffer_, filename=None, ufilename=None, desc=None):
pdf = _as_pdf_document(self)
ASSERT_PDF(pdf)
data = JM_BufferFromBytes(buffer_)
if not data.m_internal:
raise TypeError( MSG_BAD_BUFFER)
names = mupdf.pdf_dict_getl(
mupdf.pdf_trailer(pdf),
PDF_NAME('Root'),
PDF_NAME('Names'),
PDF_NAME('EmbeddedFiles'),
PDF_NAME('Names'),
)
if not mupdf.pdf_is_array(names):
root = mupdf.pdf_dict_get(mupdf.pdf_trailer(pdf), PDF_NAME('Root'))
names = mupdf.pdf_new_array(pdf, 6)
mupdf.pdf_dict_putl(
root,
names,
PDF_NAME('Names'),
PDF_NAME('EmbeddedFiles'),
PDF_NAME('Names'),
)
fileentry = JM_embed_file(pdf, data, filename, ufilename, desc, 1)
xref = mupdf.pdf_to_num(
mupdf.pdf_dict_getl(fileentry, PDF_NAME('EF'), PDF_NAME('F'))
)
mupdf.pdf_array_push(names, mupdf.pdf_new_text_string(name))
mupdf.pdf_array_push(names, fileentry)
return xref
def _embfile_del(self, idx):
pdf = _as_pdf_document(self)
names = mupdf.pdf_dict_getl(
mupdf.pdf_trailer(pdf),
PDF_NAME('Root'),
PDF_NAME('Names'),
PDF_NAME('EmbeddedFiles'),
PDF_NAME('Names'),
)
mupdf.pdf_array_delete(names, idx + 1)
mupdf.pdf_array_delete(names, idx)
def _embfile_info(self, idx, infodict):
pdf = _as_pdf_document(self)
xref = 0
ci_xref=0
trailer = mupdf.pdf_trailer(pdf)
names = mupdf.pdf_dict_getl(
trailer,
PDF_NAME('Root'),
PDF_NAME('Names'),
PDF_NAME('EmbeddedFiles'),
PDF_NAME('Names'),
)
o = mupdf.pdf_array_get(names, 2*idx+1)
ci = mupdf.pdf_dict_get(o, PDF_NAME('CI'))
if ci.m_internal:
ci_xref = mupdf.pdf_to_num(ci)
infodict["collection"] = ci_xref
name = mupdf.pdf_to_text_string(mupdf.pdf_dict_get(o, PDF_NAME('F')))
infodict[dictkey_filename] = JM_EscapeStrFromStr(name)
name = mupdf.pdf_to_text_string(mupdf.pdf_dict_get(o, PDF_NAME('UF')))
infodict[dictkey_ufilename] = JM_EscapeStrFromStr(name)
name = mupdf.pdf_to_text_string(mupdf.pdf_dict_get(o, PDF_NAME('Desc')))
infodict[dictkey_desc] = JM_UnicodeFromStr(name)
len_ = -1
DL = -1
fileentry = mupdf.pdf_dict_getl(o, PDF_NAME('EF'), PDF_NAME('F'))
xref = mupdf.pdf_to_num(fileentry)
o = mupdf.pdf_dict_get(fileentry, PDF_NAME('Length'))
if o.m_internal:
len_ = mupdf.pdf_to_int(o)
o = mupdf.pdf_dict_get(fileentry, PDF_NAME('DL'))
if o.m_internal:
DL = mupdf.pdf_to_int(o)
else:
o = mupdf.pdf_dict_getl(fileentry, PDF_NAME('Params'), PDF_NAME('Size'))
if o.m_internal:
DL = mupdf.pdf_to_int(o)
infodict[dictkey_size] = DL
infodict[dictkey_length] = len_
return xref
def _embfile_names(self, namelist):
"""Get list of embedded file names."""
if self.is_closed:
raise ValueError("document closed")
pdf = _as_pdf_document(self)
ASSERT_PDF(pdf)
names = mupdf.pdf_dict_getl(
mupdf.pdf_trailer(pdf),
PDF_NAME('Root'),
PDF_NAME('Names'),
PDF_NAME('EmbeddedFiles'),
PDF_NAME('Names'),
)
if mupdf.pdf_is_array(names):
n = mupdf.pdf_array_len(names)
for i in range(0, n, 2):
val = JM_EscapeStrFromStr(
mupdf.pdf_to_text_string(
mupdf.pdf_array_get(names, i)
)
)
namelist.append(val)
def _embfile_upd(self, idx, buffer_=None, filename=None, ufilename=None, desc=None):
pdf = _as_pdf_document(self)
xref = 0
names = mupdf.pdf_dict_getl(
mupdf.pdf_trailer(pdf),
PDF_NAME('Root'),
PDF_NAME('Names'),
PDF_NAME('EmbeddedFiles'),
PDF_NAME('Names'),
)
entry = mupdf.pdf_array_get(names, 2*idx+1)
filespec = mupdf.pdf_dict_getl(entry, PDF_NAME('EF'), PDF_NAME('F'))
if not filespec.m_internal:
RAISEPY( "bad PDF: no /EF object", JM_Exc_FileDataError)
res = JM_BufferFromBytes(buffer_)
if buffer_ and buffer_.m_internal and not res.m_internal:
raise TypeError( MSG_BAD_BUFFER)
if res.m_internal and buffer_ and buffer_.m_internal:
JM_update_stream(pdf, filespec, res, 1)
len, _ = mupdf.fz_buffer_storage(res)
l = mupdf.pdf_new_int(len)
mupdf.pdf_dict_put(filespec, PDF_NAME('DL'), l)
mupdf.pdf_dict_putl(filespec, l, PDF_NAME('Params'), PDF_NAME('Size'))
xref = mupdf.pdf_to_num(filespec)
if filename:
mupdf.pdf_dict_put_text_string(entry, PDF_NAME('F'), filename)
if ufilename:
mupdf.pdf_dict_put_text_string(entry, PDF_NAME('UF'), ufilename)
if desc:
mupdf.pdf_dict_put_text_string(entry, PDF_NAME('Desc'), desc)
return xref
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")
if g_use_extra:
return extra.Document_extend_toc_items( self.this, items)
pdf = _as_pdf_document(self)
zoom = "zoom"
bold = "bold"
italic = "italic"
collapse = "collapse"
root = mupdf.pdf_dict_get(mupdf.pdf_trailer(pdf), PDF_NAME('Root'))
if not root.m_internal:
return
olroot = mupdf.pdf_dict_get(root, PDF_NAME('Outlines'))
if not olroot.m_internal:
return
first = mupdf.pdf_dict_get(olroot, PDF_NAME('First'))
if not first.m_internal:
return
xrefs = []
xrefs = JM_outline_xrefs(first, xrefs)
n = len(xrefs)
m = len(items)
if not n:
return
if n != m:
raise IndexError( "internal error finding outline xrefs")
for i in range(n):
xref = int(xrefs[i])
item = items[i]
itemdict = item[3]
if not isinstance(itemdict, dict):
raise ValueError( "need non-simple TOC format")
itemdict[dictkey_xref] = xrefs[i]
bm = mupdf.pdf_load_object(pdf, xref)
flags = mupdf.pdf_to_int( mupdf.pdf_dict_get(bm, PDF_NAME('F')))
if flags == 1:
itemdict[italic] = True
elif flags == 2:
itemdict[bold] = True
elif flags == 3:
itemdict[italic] = True
itemdict[bold] = True
count = mupdf.pdf_to_int( mupdf.pdf_dict_get(bm, PDF_NAME('Count')))
if count < 0:
itemdict[collapse] = True
elif count > 0:
itemdict[collapse] = False
col = mupdf.pdf_dict_get(bm, PDF_NAME('C'))
if mupdf.pdf_is_array(col) and mupdf.pdf_array_len(col) == 3:
color = (
mupdf.pdf_to_real(mupdf.pdf_array_get(col, 0)),
mupdf.pdf_to_real(mupdf.pdf_array_get(col, 1)),
mupdf.pdf_to_real(mupdf.pdf_array_get(col, 2)),
)
itemdict[dictkey_color] = color
z=0
obj = mupdf.pdf_dict_get(bm, PDF_NAME('Dest'))
if not obj.m_internal or not mupdf.pdf_is_array(obj):
obj = mupdf.pdf_dict_getl(bm, PDF_NAME('A'), PDF_NAME('D'))
if mupdf.pdf_is_array(obj) and mupdf.pdf_array_len(obj) == 5:
z = mupdf.pdf_to_real(mupdf.pdf_array_get(obj, 4))
itemdict[zoom] = float(z)
item[3] = itemdict
items[i] = item
def _forget_page(self, page: Page):
"""Remove a page from document page dict."""
pid = id(page)
if pid in self._page_refs:
del self._page_refs[pid]
def _get_char_widths(self, xref: int, bfname: str, ext: str, ordering: int, limit: int, idx: int = 0):
pdf = _as_pdf_document(self)
mylimit = limit
if mylimit < 256:
mylimit = 256
ASSERT_PDF(pdf), f'{pdf=}'
if ordering >= 0:
data, size, index = mupdf.fz_lookup_cjk_font(ordering)
font = mupdf.fz_new_font_from_memory(None, data, size, index, 0)
else:
data, size = mupdf.fz_lookup_base14_font(bfname)
if data:
font = mupdf.fz_new_font_from_memory(bfname, data, size, 0, 0)
else:
buf = JM_get_fontbuffer(pdf, xref)
if not buf.m_internal:
raise Exception("font at xref %d is not supported" % xref)
font = mupdf.fz_new_font_from_buffer(None, buf, idx, 0)
wlist = []
for i in range(mylimit):
glyph = mupdf.fz_encode_character(font, i)
adv = mupdf.fz_advance_glyph(font, glyph, 0)
if ordering >= 0:
glyph = i
if glyph > 0:
wlist.append( (glyph, adv))
else:
wlist.append( (glyph, 0.0))
return wlist
def _get_page_labels(self):
pdf = _as_pdf_document(self)
ASSERT_PDF(pdf)
rc = []
pagelabels = mupdf.pdf_new_name("PageLabels")
obj = mupdf.pdf_dict_getl( mupdf.pdf_trailer(pdf), PDF_NAME('Root'), pagelabels)
if not obj.m_internal:
return rc
nums = mupdf.pdf_resolve_indirect( mupdf.pdf_dict_get( obj, PDF_NAME('Nums')))
if nums.m_internal:
JM_get_page_labels(rc, nums)
return rc
nums = mupdf.pdf_resolve_indirect( mupdf.pdf_dict_getl(obj, PDF_NAME('Kids'), PDF_NAME('Nums')))
if nums.m_internal:
JM_get_page_labels(rc, nums)
return rc
kids = mupdf.pdf_resolve_indirect( mupdf.pdf_dict_get( obj, PDF_NAME('Kids')))
if not kids.m_internal or not mupdf.pdf_is_array(kids):
return rc
n = mupdf.pdf_array_len(kids)
for i in range(n):
nums = mupdf.pdf_resolve_indirect(
mupdf.pdf_dict_get( mupdf.pdf_array_get(kids, i)),
PDF_NAME('Nums'),
)
JM_get_page_labels(rc, nums)
return rc
def _getMetadata(self, key):
"""Get metadata."""
try:
return mupdf.fz_lookup_metadata2( self.this, key)
except Exception:
return ''
def _getOLRootNumber(self):
"""Get xref of Outline Root, create it if missing."""
if self.is_closed or self.isEncrypted:
raise ValueError("document closed or encrypted")
pdf = _as_pdf_document(self)
ASSERT_PDF(pdf)
root = mupdf.pdf_dict_get( mupdf.pdf_trailer( pdf), PDF_NAME('Root'))
olroot = mupdf.pdf_dict_get( root, PDF_NAME('Outlines'))
if not olroot.m_internal:
olroot = mupdf.pdf_new_dict( pdf, 4)
mupdf.pdf_dict_put( olroot, PDF_NAME('Type'), PDF_NAME('Outlines'))
ind_obj = mupdf.pdf_add_object( pdf, olroot)
mupdf.pdf_dict_put( root, PDF_NAME('Outlines'), ind_obj)
olroot = mupdf.pdf_dict_get( root, PDF_NAME('Outlines'))
return mupdf.pdf_to_num( olroot)
def _getPDFfileid(self):
"""Get PDF file id."""
if self.is_closed:
raise ValueError("document closed")
pdf = _as_pdf_document(self)
if not pdf:
return
idlist = []
identity = mupdf.pdf_dict_get(mupdf.pdf_trailer(pdf), PDF_NAME('ID'))
if identity.m_internal:
n = mupdf.pdf_array_len(identity)
for i in range(n):
o = mupdf.pdf_array_get(identity, i)
text = mupdf.pdf_to_text_string(o)
hex_ = binascii.hexlify(text)
idlist.append(hex_)
return idlist
def _getPageInfo(self, pno, what):
"""List fonts, images, XObjects used on a page."""
if self.is_closed or self.isEncrypted:
raise ValueError("document closed or encrypted")
doc = self.this
pdf = _as_pdf_document(self)
pageCount = mupdf.pdf_count_pages(doc) if isinstance(doc, mupdf.PdfDocument) else mupdf.fz_count_pages(doc)
n = pno
while n < 0:
n += pageCount
if n >= pageCount:
raise ValueError( MSG_BAD_PAGENO)
pageref = mupdf.pdf_lookup_page_obj(pdf, n)
rsrc = mupdf.pdf_dict_get_inheritable(pageref, mupdf.PDF_ENUM_NAME_Resources)
liste = []
tracer = []
if rsrc.m_internal:
JM_scan_resources(pdf, rsrc, liste, what, 0, tracer)
return liste
def _insert_font(self, fontfile=None, fontbuffer=None):
'''
Utility: insert font from file or binary.
'''
pdf = _as_pdf_document(self)
ASSERT_PDF(pdf)
if not fontfile and not fontbuffer:
raise ValueError( MSG_FILE_OR_BUFFER)
value = JM_insert_font(pdf, None, fontfile, fontbuffer, 0, 0, 0, 0, 0, -1)
return value
def _loadOutline(self):
"""Load first outline."""
doc = self.this
assert isinstance( doc, mupdf.FzDocument)
try:
ol = mupdf.fz_load_outline( doc)
except Exception:
if 0 and g_exceptions_verbose: exception_info()
return
return Outline( ol)
def _make_page_map(self):
"""Make an array page number -> page object."""
if self.is_closed:
raise ValueError("document closed")
assert 0, f'_make_page_map() is no-op'
def _move_copy_page(self, pno, nb, before, copy):
"""Move or copy a PDF page reference."""
if self.is_closed:
raise ValueError("document closed")
pdf = _as_pdf_document(self)
same = 0
ASSERT_PDF(pdf)
page1, parent1, i1 = pdf_lookup_page_loc( pdf, pno)
kids1 = mupdf.pdf_dict_get( parent1, PDF_NAME('Kids'))
page2, parent2, i2 = pdf_lookup_page_loc( pdf, nb)
kids2 = mupdf.pdf_dict_get( parent2, PDF_NAME('Kids'))
if before:
pos = i2
else:
pos = i2 + 1
same = mupdf.pdf_objcmp( kids1, kids2)
if not copy and same != 0:
mupdf.pdf_dict_put( page1, PDF_NAME('Parent'), parent2)
mupdf.pdf_array_insert( kids2, page1, pos)
if same != 0:
parent = parent2
while parent.m_internal:
count = mupdf.pdf_dict_get_int( parent, PDF_NAME('Count'))
mupdf.pdf_dict_put_int( parent, PDF_NAME('Count'), count + 1)
parent = mupdf.pdf_dict_get( parent, PDF_NAME('Parent'))
if not copy:
mupdf.pdf_array_delete( kids1, i1)
parent = parent1
while parent.m_internal:
count = mupdf.pdf_dict_get_int( parent, PDF_NAME('Count'))
mupdf.pdf_dict_put_int( parent, PDF_NAME('Count'), count - 1)
parent = mupdf.pdf_dict_get( parent, PDF_NAME('Parent'))
else:
if copy:
parent = parent2
while parent.m_internal:
count = mupdf.pdf_dict_get_int( parent, PDF_NAME('Count'))
mupdf.pdf_dict_put_int( parent, PDF_NAME('Count'), count + 1)
parent = mupdf.pdf_dict_get( parent, PDF_NAME('Parent'))
else:
if i1 < pos:
mupdf.pdf_array_delete( kids1, i1)
else:
mupdf.pdf_array_delete( kids1, i1 + 1)
if pdf.m_internal.rev_page_map:
mupdf.ll_pdf_drop_page_tree( pdf.m_internal)
self._reset_page_refs()
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")
if g_use_extra:
extra._newPage( self.this, pno, width, height)
else:
pdf = _as_pdf_document(self)
assert isinstance(pdf, mupdf.PdfDocument)
mediabox = mupdf.FzRect(mupdf.FzRect.Fixed_UNIT)
mediabox.x1 = width
mediabox.y1 = height
contents = mupdf.FzBuffer()
if pno < -1:
raise ValueError( MSG_BAD_PAGENO)
resources = mupdf.pdf_add_new_dict(pdf, 1)
page_obj = mupdf.pdf_add_page( pdf, mediabox, 0, resources, contents)
mupdf.pdf_insert_page( pdf, pno, page_obj)
self._reset_page_refs()
return self[pno]
def _remove_links_to(self, numbers):
pdf = _as_pdf_document(self)
_remove_dest_range(pdf, numbers)
def _remove_toc_item(self, xref):
pdf = _as_pdf_document(self)
item = mupdf.pdf_new_indirect(pdf, xref, 0)
mupdf.pdf_dict_del( item, PDF_NAME('Dest'))
mupdf.pdf_dict_del( item, PDF_NAME('A'))
color = mupdf.pdf_new_array( pdf, 3)
for i in range(3):
mupdf.pdf_array_push_real( color, 0.8)
mupdf.pdf_dict_put( item, PDF_NAME('C'), color)
def _reset_page_refs(self):
"""Invalidate all pages in document dictionary."""
if getattr(self, "is_closed", True):
return
self._page_refs.clear()
def _set_page_labels(self, labels):
pdf = _as_pdf_document(self)
ASSERT_PDF(pdf)
pagelabels = mupdf.pdf_new_name("PageLabels")
root = mupdf.pdf_dict_get(mupdf.pdf_trailer(pdf), PDF_NAME('Root'))
mupdf.pdf_dict_del(root, pagelabels)
mupdf.pdf_dict_putl(root, mupdf.pdf_new_array(pdf, 0), pagelabels, PDF_NAME('Nums'))
xref = self.pdf_catalog()
text = self.xref_object(xref, compressed=True)
text = text.replace("/Nums[]", "/Nums[%s]" % labels)
self.update_object(xref, text)
def _this_as_document(self):
'''
Returns self.this as a mupdf.FzDocument.
'''
if isinstance(self.this, mupdf.FzDocument):
return self.this
if isinstance(self.this, mupdf.PdfDocument):
return self.this.super()
assert 0, f'Unrecognised {type(self.this)=}'
def _this_as_pdf_document(self):
'''
Returns self.this as a mupdf.PdfDocument, downcasting as required. If
we fail (i.e. self.this is a mupdf.FzDocument(), <ret>.m_internal will be
None.
'''
if isinstance(self.this, mupdf.PdfDocument):
return self.this
if isinstance(self.this, mupdf.FzDocument):
return mupdf.PdfDocument(self.this)
assert 0, f'Unrecognised {type(self.this)=}'
def _update_toc_item(self, xref, action=None, title=None, flags=0, collapse=None, color=None):
'''
"update" bookmark by letting it point to nowhere
'''
pdf = _as_pdf_document(self)
item = mupdf.pdf_new_indirect( pdf, xref, 0)
if title:
mupdf.pdf_dict_put_text_string( item, PDF_NAME('Title'), title)
if action:
mupdf.pdf_dict_del( item, PDF_NAME('Dest'))
obj = JM_pdf_obj_from_str( pdf, action)
mupdf.pdf_dict_put( item, PDF_NAME('A'), obj)
mupdf.pdf_dict_put_int( item, PDF_NAME('F'), flags)
if color:
c = mupdf.pdf_new_array( pdf, 3)
for i in range(3):
f = color[i]
mupdf.pdf_array_push_real( c, f)
mupdf.pdf_dict_put( item, PDF_NAME('C'), c)
elif color is not None:
mupdf.pdf_dict_del( item, PDF_NAME('C'))
if collapse is not None:
if mupdf.pdf_dict_get( item, PDF_NAME('Count')).m_internal:
i = mupdf.pdf_dict_get_int( item, PDF_NAME('Count'))
if (i < 0 and collapse is False) or (i > 0 and collapse is True):
i = i * (-1)
mupdf.pdf_dict_put_int( item, PDF_NAME('Count'), i)
@property
def FormFonts(self):
"""Get list of field font resource names."""
if self.is_closed:
raise ValueError("document closed")
pdf = _as_pdf_document(self)
if not pdf:
return
fonts = mupdf.pdf_dict_getl(
mupdf.pdf_trailer(pdf),
PDF_NAME('Root'),
PDF_NAME('AcroForm'),
PDF_NAME('DR'),
PDF_NAME('Font'),
)
liste = list()
if fonts.m_internal and mupdf.pdf_is_dict(fonts):
n = mupdf.pdf_dict_len(fonts)
for i in range(n):
f = mupdf.pdf_dict_get_key(fonts, i)
liste.append(JM_UnicodeFromStr(mupdf.pdf_to_name(f)))
return liste
def add_layer(self, name, creator=None, on=None):
"""Add a new OC layer."""
if self.is_closed:
raise ValueError("document closed")
pdf = _as_pdf_document(self)
ASSERT_PDF(pdf)
JM_add_layer_config( pdf, name, creator, on)
mupdf.ll_pdf_read_ocg( pdf.m_internal)
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")
xref = 0
pdf = _as_pdf_document(self)
ASSERT_PDF(pdf)
ocg = mupdf.pdf_add_new_dict(pdf, 3)
mupdf.pdf_dict_put(ocg, PDF_NAME('Type'), PDF_NAME('OCG'))
mupdf.pdf_dict_put_text_string(ocg, PDF_NAME('Name'), name)
intents = mupdf.pdf_dict_put_array(ocg, PDF_NAME('Intent'), 2)
if not intent:
mupdf.pdf_array_push(intents, PDF_NAME('View'))
elif not isinstance(intent, str):
assert 0, f'fixme: intent is not a str. {type(intent)=} {type=}'
else:
mupdf.pdf_array_push(intents, mupdf.pdf_new_name(intent))
use_for = mupdf.pdf_dict_put_dict(ocg, PDF_NAME('Usage'), 3)
ci_name = mupdf.pdf_new_name("CreatorInfo")
cre_info = mupdf.pdf_dict_put_dict(use_for, ci_name, 2)
mupdf.pdf_dict_put_text_string(cre_info, PDF_NAME('Creator'), "PyMuPDF")
if usage:
mupdf.pdf_dict_put_name(cre_info, PDF_NAME('Subtype'), usage)
else:
mupdf.pdf_dict_put_name(cre_info, PDF_NAME('Subtype'), "Artwork")
indocg = mupdf.pdf_add_object(pdf, ocg)
ocp = JM_ensure_ocproperties(pdf)
obj = mupdf.pdf_dict_get(ocp, PDF_NAME('OCGs'))
mupdf.pdf_array_push(obj, indocg)
if config > -1:
obj = mupdf.pdf_dict_get(ocp, PDF_NAME('Configs'))
if not mupdf.pdf_is_array(obj):
raise ValueError( MSG_BAD_OC_CONFIG)
cfg = mupdf.pdf_array_get(obj, config)
if not cfg.m_internal:
raise ValueError( MSG_BAD_OC_CONFIG)
else:
cfg = mupdf.pdf_dict_get(ocp, PDF_NAME('D'))
obj = mupdf.pdf_dict_get(cfg, PDF_NAME('Order'))
if not obj.m_internal:
obj = mupdf.pdf_dict_put_array(cfg, PDF_NAME('Order'), 1)
mupdf.pdf_array_push(obj, indocg)
if on:
obj = mupdf.pdf_dict_get(cfg, PDF_NAME('ON'))
if not obj.m_internal:
obj = mupdf.pdf_dict_put_array(cfg, PDF_NAME('ON'), 1)
else:
obj =mupdf.pdf_dict_get(cfg, PDF_NAME('OFF'))
if not obj.m_internal:
obj =mupdf.pdf_dict_put_array(cfg, PDF_NAME('OFF'), 1)
mupdf.pdf_array_push(obj, indocg)
mupdf.ll_pdf_read_ocg(pdf.m_internal)
xref = mupdf.pdf_to_num(indocg)
return xref
def authenticate(self, password):
"""Decrypt document."""
if self.is_closed:
raise ValueError("document closed")
val = mupdf.fz_authenticate_password(self.this, password)
if val:
self.is_encrypted = False
self.isEncrypted = False
self.init_doc()
self.thisown = True
return val
def can_save_incrementally(self):
"""Check whether incremental saves are possible."""
if self.is_closed:
raise ValueError("document closed")
pdf = _as_pdf_document(self)
if not pdf:
return False
return mupdf.pdf_can_be_saved_incrementally(pdf)
@property
def chapter_count(self):
"""Number of chapters."""
if self.is_closed:
raise ValueError("document closed")
return mupdf.fz_count_chapters( self.this)
def chapter_page_count(self, chapter):
"""Page count of chapter."""
if self.is_closed:
raise ValueError("document closed")
chapters = mupdf.fz_count_chapters( self.this)
if chapter < 0 or chapter >= chapters:
raise ValueError( "bad chapter number")
pages = mupdf.fz_count_chapter_pages( self.this, chapter)
return pages
def close(self):
"""Close document."""
if getattr(self, "is_closed", True):
raise ValueError("document closed")
if hasattr(self, "_outline") and self._outline:
self._outline = None
self.close_internal()
def close_internal(self):
self.this = None
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.isEncrypted:
raise ValueError("document closed or encrypted")
fz_doc = self.this
fp = from_page
tp = to_page
srcCount = mupdf.fz_count_pages(fz_doc)
if fp < 0:
fp = 0
if fp > srcCount - 1:
fp = srcCount - 1
if tp < 0:
tp = srcCount - 1
if tp > srcCount - 1:
tp = srcCount - 1
len0 = len(JM_mupdf_warnings_store)
doc = JM_convert_to_pdf(fz_doc, fp, tp, rotate)
len1 = len(JM_mupdf_warnings_store)
for i in range(len0, len1):
PySys_WriteStderr(f'{JM_mupdf_warnings_store[i]}\n')
return doc
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 del_xml_metadata(self):
"""Delete XML metadata."""
if self.is_closed or self.isEncrypted:
raise ValueError("document closed or encrypted")
pdf = _as_pdf_document(self)
root = mupdf.pdf_dict_get( mupdf.pdf_trailer( pdf), PDF_NAME('Root'))
if root.m_internal:
mupdf.pdf_dict_del( root, PDF_NAME('Metadata'))
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)")
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
f = t = -1
if kw:
if args:
raise ValueError("cannot mix keyword and positional argument")
f = kw.get("from_page", -1)
t = kw.get("to_page", -1)
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)))
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)
self._remove_links_to(frozen_numbers)
for i in reversed(numbers):
self._delete_page(i)
self._reset_page_refs()
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 = 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 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_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_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_names(self) -> list:
"""Get list of names of EmbeddedFiles."""
filenames = []
self._embfile_names(filenames)
return filenames
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 extract_font(self, xref=0, info_only=0, named=None):
'''
Get a font by xref. Returns a tuple or dictionary.
'''
pdf = _as_pdf_document(self)
ASSERT_PDF(pdf)
obj = mupdf.pdf_load_object(pdf, xref)
type_ = mupdf.pdf_dict_get(obj, PDF_NAME('Type'))
subtype = mupdf.pdf_dict_get(obj, PDF_NAME('Subtype'))
if (mupdf.pdf_name_eq(type_, PDF_NAME('Font'))
and not mupdf.pdf_to_name( subtype).startswith('CIDFontType')
):
basefont = mupdf.pdf_dict_get(obj, PDF_NAME('BaseFont'))
if not basefont.m_internal or mupdf.pdf_is_null(basefont):
bname = mupdf.pdf_dict_get(obj, PDF_NAME('Name'))
else:
bname = basefont
ext = JM_get_fontextension(pdf, xref)
if ext != 'n/a' and not info_only:
buffer_ = JM_get_fontbuffer(pdf, xref)
bytes_ = JM_BinFromBuffer(buffer_)
else:
bytes_ = b''
if not named:
rc = (
JM_EscapeStrFromStr(mupdf.pdf_to_name(bname)),
JM_UnicodeFromStr(ext),
JM_UnicodeFromStr(mupdf.pdf_to_name(subtype)),
bytes_,
)
else:
rc = {
dictkey_name: JM_EscapeStrFromStr(mupdf.pdf_to_name(bname)),
dictkey_ext: JM_UnicodeFromStr(ext),
dictkey_type: JM_UnicodeFromStr(mupdf.pdf_to_name(subtype)),
dictkey_content: bytes_,
}
else:
if not named:
rc = '', '', '', b''
else:
rc = {
dictkey_name: '',
dictkey_ext: '',
dictkey_type: '',
dictkey_content: b'',
}
return rc
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")
pdf = _as_pdf_document(self)
img_type = 0
smask = 0
ASSERT_PDF(pdf)
if not _INRANGE(xref, 1, mupdf.pdf_xref_len(pdf)-1):
raise ValueError( MSG_BAD_XREF)
obj = mupdf.pdf_new_indirect(pdf, xref, 0)
subtype = mupdf.pdf_dict_get(obj, PDF_NAME('Subtype'))
if not mupdf.pdf_name_eq(subtype, PDF_NAME('Image')):
raise ValueError( "not an image")
o = mupdf.pdf_dict_geta(obj, PDF_NAME('SMask'), PDF_NAME('Mask'))
if o.m_internal:
smask = mupdf.pdf_to_num(o)
if mupdf.pdf_is_jpx_image(obj):
img_type = mupdf.FZ_IMAGE_JPX
res = mupdf.pdf_load_stream(obj)
ext = "jpx"
if JM_is_jbig2_image(obj):
img_type = mupdf.FZ_IMAGE_JBIG2
res = mupdf.pdf_load_stream(obj)
ext = "jb2"
res = mupdf.pdf_load_raw_stream(obj)
if img_type == mupdf.FZ_IMAGE_UNKNOWN:
res = mupdf.pdf_load_raw_stream(obj)
_, c = mupdf.fz_buffer_storage(res)
img_type = mupdf.fz_recognize_image_format(c)
ext = JM_image_extension(img_type)
if img_type == mupdf.FZ_IMAGE_UNKNOWN:
res = None
img = mupdf.pdf_load_image(pdf, obj)
ll_cbuf = mupdf.ll_fz_compressed_image_buffer(img.m_internal)
if (ll_cbuf
and ll_cbuf.params.type not in (
mupdf.FZ_IMAGE_RAW,
mupdf.FZ_IMAGE_FAX,
mupdf.FZ_IMAGE_FLATE,
mupdf.FZ_IMAGE_LZW,
mupdf.FZ_IMAGE_RLD,
)
):
img_type = ll_cbuf.params.type
ext = JM_image_extension(img_type)
res = mupdf.FzBuffer(mupdf.ll_fz_keep_buffer(ll_cbuf.buffer))
else:
res = mupdf.fz_new_buffer_from_image_as_png(
img,
mupdf.FzColorParams(mupdf.fz_default_color_params),
)
ext = "png"
else:
img = mupdf.fz_new_image_from_buffer(res)
xres, yres = mupdf.fz_image_resolution(img)
width = img.w()
height = img.h()
colorspace = img.n()
bpc = img.bpc()
cs_name = mupdf.fz_colorspace_name(img.colorspace())
rc = dict()
rc[ dictkey_ext] = ext
rc[ dictkey_smask] = smask
rc[ dictkey_width] = width
rc[ dictkey_height] = height
rc[ dictkey_colorspace] = colorspace
rc[ dictkey_bpc] = bpc
rc[ dictkey_xres] = xres
rc[ dictkey_yres] = yres
rc[ dictkey_cs_name] = cs_name
rc[ dictkey_image] = JM_BinFromBuffer(res)
return rc
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 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")
location = mupdf.fz_lookup_bookmark2( self.this, bm)
return location.chapter, location.page
def fullcopy_page(self, pno, to=-1):
"""Make a full page duplicate."""
if self.is_closed:
raise ValueError("document closed")
pdf = _as_pdf_document(self)
page_count = mupdf.pdf_count_pages( pdf)
try:
ASSERT_PDF(pdf)
if (not _INRANGE(pno, 0, page_count - 1)
or not _INRANGE(to, -1, page_count - 1)
):
raise ValueError( MSG_BAD_PAGENO)
page1 = mupdf.pdf_resolve_indirect( mupdf.pdf_lookup_page_obj( pdf, pno))
page2 = mupdf.pdf_deep_copy_obj( page1)
old_annots = mupdf.pdf_dict_get( page2, PDF_NAME('Annots'))
if old_annots.m_internal:
n = mupdf.pdf_array_len( old_annots)
new_annots = mupdf.pdf_new_array( pdf, n)
for i in range(n):
o = mupdf.pdf_array_get( old_annots, i)
subtype = mupdf.pdf_dict_get( o, PDF_NAME('Subtype'))
if mupdf.pdf_name_eq( subtype, PDF_NAME('Popup')):
continue
if mupdf.pdf_dict_gets( o, "IRT").m_internal:
continue
copy_o = mupdf.pdf_deep_copy_obj( mupdf.pdf_resolve_indirect( o))
xref = mupdf.pdf_create_object( pdf)
mupdf.pdf_update_object( pdf, xref, copy_o)
copy_o = mupdf.pdf_new_indirect( pdf, xref, 0)
mupdf.pdf_dict_del( copy_o, PDF_NAME('Popup'))
mupdf.pdf_dict_del( copy_o, PDF_NAME('P'))
mupdf.pdf_array_push( new_annots, copy_o)
mupdf.pdf_dict_put( page2, PDF_NAME('Annots'), new_annots)
res = JM_read_contents( page1)
if res.m_internal:
contents = mupdf.pdf_add_stream( pdf, mupdf.fz_new_buffer_from_copied_data( b" "), mupdf.PdfObj(), 0)
JM_update_stream( pdf, contents, res, 1)
mupdf.pdf_dict_put( page2, PDF_NAME('Contents'), contents)
xref = mupdf.pdf_create_object( pdf)
mupdf.pdf_update_object( pdf, xref, page2)
page2 = mupdf.pdf_new_indirect( pdf, xref, 0)
mupdf.pdf_insert_page( pdf, to, page2)
finally:
mupdf.ll_pdf_drop_page_tree( pdf.m_internal)
self._reset_page_refs()
def get_layer(self, config=-1):
"""Content of ON, OFF, RBGroups of an OC layer."""
if self.is_closed:
raise ValueError("document closed")
pdf = _as_pdf_document(self)
ASSERT_PDF(pdf)
ocp = mupdf.pdf_dict_getl(
mupdf.pdf_trailer( pdf),
PDF_NAME('Root'),
PDF_NAME('OCProperties'),
)
if not ocp.m_internal:
return
if config == -1:
obj = mupdf.pdf_dict_get( ocp, PDF_NAME('D'))
else:
obj = mupdf.pdf_array_get(
mupdf.pdf_dict_get( ocp, PDF_NAME('Configs')),
config,
)
if not obj.m_internal:
raise ValueError( MSG_BAD_OC_CONFIG)
rc = JM_get_ocg_arrays( obj)
return rc
def get_layers(self):
"""Show optional OC layers."""
if self.is_closed:
raise ValueError("document closed")
pdf = _as_pdf_document(self)
ASSERT_PDF(pdf)
n = mupdf.pdf_count_layer_configs( pdf)
if n == 1:
obj = mupdf.pdf_dict_getl(
mupdf.pdf_trailer( pdf),
PDF_NAME('Root'),
PDF_NAME('OCProperties'),
PDF_NAME('Configs'),
)
if not mupdf.pdf_is_array( obj):
n = 0
rc = []
info = mupdf.PdfLayerConfig()
for i in range(n):
mupdf.pdf_layer_config_info( pdf, i, info)
item = {
"number": i,
"name": info.name,
"creator": info.creator,
}
rc.append( item)
return rc
def get_new_xref(self):
"""Make new xref."""
if self.is_closed or self.isEncrypted:
raise ValueError("document closed or encrypted")
pdf = _as_pdf_document(self)
xref = 0
ASSERT_PDF(pdf)
ENSURE_OPERATION(pdf)
xref = mupdf.pdf_create_object(pdf)
return xref
def get_ocgs(self):
"""Show existing optional content groups."""
if self.is_closed:
raise ValueError("document closed")
ci = mupdf.pdf_new_name( "CreatorInfo")
pdf = _as_pdf_document(self)
ASSERT_PDF(pdf)
ocgs = mupdf.pdf_dict_getl(
mupdf.pdf_dict_get( mupdf.pdf_trailer( pdf), PDF_NAME('Root')),
PDF_NAME('OCProperties'),
PDF_NAME('OCGs'),
)
rc = dict()
if not mupdf.pdf_is_array( ocgs):
return rc
n = mupdf.pdf_array_len( ocgs)
for i in range(n):
ocg = mupdf.pdf_array_get( ocgs, i)
xref = mupdf.pdf_to_num( ocg)
name = mupdf.pdf_to_text_string( mupdf.pdf_dict_get( ocg, PDF_NAME('Name')))
obj = mupdf.pdf_dict_getl( ocg, PDF_NAME('Usage'), ci, PDF_NAME('Subtype'))
usage = None
if obj.m_internal:
usage = mupdf.pdf_to_name( obj)
intents = list()
intent = mupdf.pdf_dict_get( ocg, PDF_NAME('Intent'))
if intent.m_internal:
if mupdf.pdf_is_name( intent):
intents.append( mupdf.pdf_to_name( intent))
elif mupdf.pdf_is_array( intent):
m = mupdf.pdf_array_len( intent)
for j in range(m):
o = mupdf.pdf_array_get( intent, j)
if mupdf.pdf_is_name( o):
intents.append( mupdf.pdf_to_name( o))
hidden = mupdf.pdf_is_ocg_hidden( pdf, mupdf.PdfObj(), usage, ocg)
item = {
"name": name,
"intent": intents,
"on": not hidden,
"usage": usage,
}
temp = xref
rc[ temp] = item
return rc
def get_outline_xrefs(self):
"""Get list of outline xref numbers."""
if self.is_closed:
raise ValueError("document closed")
xrefs = []
pdf = _as_pdf_document(self)
if not pdf:
return xrefs
root = mupdf.pdf_dict_get(mupdf.pdf_trailer(pdf), PDF_NAME('Root'))
if not root.m_internal:
return xrefs
olroot = mupdf.pdf_dict_get(root, PDF_NAME('Outlines'))
if not olroot.m_internal:
return xrefs
first = mupdf.pdf_dict_get(olroot, PDF_NAME('First'))
if not first.m_internal:
return xrefs
xrefs = JM_outline_xrefs(first, xrefs)
return xrefs
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 Exception:
exception_info()
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.isEncrypted:
raise ValueError("document closed or encrypted")
if not self.is_pdf:
return ()
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.isEncrypted:
raise ValueError("document closed or encrypted")
if not self.is_pdf:
return ()
val = self._getPageInfo(pno, 3)
return val
def get_sigflags(self):
"""Get the /SigFlags value."""
if self.is_closed:
raise ValueError("document closed")
pdf = _as_pdf_document(self)
if not pdf:
return -1
sigflags = mupdf.pdf_dict_getl(
mupdf.pdf_trailer(pdf),
PDF_NAME('Root'),
PDF_NAME('AcroForm'),
PDF_NAME('SigFlags'),
)
sigflag = -1
if sigflags.m_internal:
sigflag = mupdf.pdf_to_int(sigflags)
return sigflag
def get_xml_metadata(self):
"""Get document XML metadata."""
if self.is_closed:
raise ValueError("document closed")
xml = None
pdf = _as_pdf_document(self)
if pdf.m_internal:
xml = mupdf.pdf_dict_getl(
mupdf.pdf_trailer(pdf),
PDF_NAME('Root'),
PDF_NAME('Metadata'),
)
if xml and xml.internal:
buff = mupdf.pdf_load_stream(xml)
rc = JM_UnicodeFromBuffer(buff)
else:
rc = ''
return rc
def init_doc(self):
if self.is_encrypted:
raise ValueError("cannot initialize - document still encrypted")
self._outline = self._loadOutline()
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')
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 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))
isrt = docsrc._graft_id
_gmap = self.Graftmaps.get(isrt, None)
if _gmap is None:
_gmap = Graftmap(self)
self.Graftmaps[isrt] = _gmap
if g_use_extra:
extra_FzDocument_insert_pdf(
self.this,
docsrc.this,
from_page,
to_page,
start_at,
rotate,
links,
annots,
show_progress,
final,
_gmap,
)
else:
pdfout = _as_pdf_document(self)
pdfsrc = _as_pdf_document(docsrc)
outCount = mupdf.fz_count_pages(self)
srcCount = mupdf.fz_count_pages(docsrc.this)
fp = from_page
tp = to_page
sa = start_at
fp = max(fp, 0)
fp = min(fp, srcCount - 1)
if tp < 0:
tp = srcCount - 1
tp = min(tp, srcCount - 1)
if sa < 0:
sa = outCount
sa = min(sa, outCount)
if not pdfout.m_internal or not pdfsrc.m_internal:
raise TypeError( "source or target not a PDF")
ENSURE_OPERATION(pdfout)
JM_merge_range(pdfout, pdfsrc, fp, tp, sa, rotate, links, annots, show_progress, _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
@property
def is_dirty(self):
pdf = _as_pdf_document(self)
if not pdf:
return False
r = mupdf.pdf_has_unsaved_changes(pdf)
return True if r else False
@property
def is_fast_webaccess(self):
'''
Check whether we have a linearized PDF.
'''
pdf = _as_pdf_document(self)
if pdf.m_internal:
return mupdf.pdf_doc_was_linearized(pdf)
return False
@property
def is_form_pdf(self):
"""Either False or PDF field count."""
pdf = _as_pdf_document(self)
if not pdf:
return False
count = -1
try:
fields = mupdf.pdf_dict_getl(
mupdf.pdf_trailer(pdf),
mupdf.PDF_ENUM_NAME_Root,
mupdf.PDF_ENUM_NAME_AcroForm,
mupdf.PDF_ENUM_NAME_Fields,
)
if mupdf.pdf_is_array(fields):
count = mupdf.pdf_array_len(fields)
except Exception:
if g_exceptions_verbose: exception_info()
return False
if count >= 0:
return count
return False
@property
def is_pdf(self):
"""Check for PDF."""
if isinstance(self.this, mupdf.PdfDocument):
return True
if mupdf.ll_pdf_specifics(self.this.m_internal):
ret = True
else:
ret = False
return ret
@property
def is_reflowable(self):
"""Check if document is layoutable."""
if self.is_closed:
raise ValueError("document closed")
return mupdf.fz_is_document_reflowable(self._document())
@property
def is_repaired(self):
"""Check whether PDF was repaired."""
pdf = _as_pdf_document(self)
if not pdf:
return False
r = mupdf.pdf_was_repaired(pdf)
if r:
return True
return False
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")
undo=0
redo=0
pdf = _as_pdf_document(self)
ASSERT_PDF(pdf)
undo = mupdf.pdf_can_undo(pdf)
redo = mupdf.pdf_can_redo(pdf)
return {'undo': bool(undo), 'redo': bool(redo)}
def journal_enable(self):
"""Activate document journalling."""
if self.is_closed or self.is_encrypted:
raise ValueError("document closed or encrypted")
pdf = _as_pdf_document(self)
ASSERT_PDF(pdf)
mupdf.pdf_enable_journal(pdf)
def journal_is_enabled(self):
"""Check if journalling is enabled."""
if self.is_closed or self.is_encrypted:
raise ValueError("document closed or encrypted")
pdf = _as_pdf_document(self)
enabled = pdf.m_internal and pdf.m_internal.journal
return enabled
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")
pdf = _as_pdf_document(self)
ASSERT_PDF(pdf)
if isinstance(filename, str):
mupdf.pdf_load_journal(pdf, filename)
else:
res = JM_BufferFromBytes(filename)
stm = mupdf.fz_open_buffer(res)
mupdf.pdf_deserialise_journal(pdf, stm)
if not pdf.m_internal.journal:
RAISEPY( "Journal and document do not match", JM_Exc_FileDataError)
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")
pdf = _as_pdf_document(self)
ASSERT_PDF(pdf)
name = mupdf.pdf_undoredo_step(pdf, step)
return name
def journal_position(self):
"""Show journalling state."""
if self.is_closed or self.is_encrypted:
raise ValueError("document closed or encrypted")
steps=0
pdf = _as_pdf_document(self)
ASSERT_PDF(pdf)
rc, steps = mupdf.pdf_undoredo_state(pdf)
return rc, steps
def journal_redo(self):
"""Move forward in the journal."""
if self.is_closed or self.is_encrypted:
raise ValueError("document closed or encrypted")
pdf = _as_pdf_document(self)
ASSERT_PDF(pdf)
mupdf.pdf_redo(pdf)
return True
def journal_save(self, filename):
"""Save journal to a file."""
if self.is_closed or self.is_encrypted:
raise ValueError("document closed or encrypted")
pdf = _as_pdf_document(self)
ASSERT_PDF(pdf)
if isinstance(filename, str):
mupdf.pdf_save_journal(pdf, filename)
else:
out = JM_new_output_fileptr(filename)
mupdf.pdf_write_journal(pdf, out)
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")
pdf = _as_pdf_document(self)
ASSERT_PDF(pdf)
if not pdf.m_internal.journal:
raise RuntimeError( "Journalling not enabled")
if name:
mupdf.pdf_begin_operation(pdf, name)
else:
mupdf.pdf_begin_implicit_operation(pdf)
def journal_stop_op(self):
"""End a journalling operation."""
if self.is_closed or self.is_encrypted:
raise ValueError("document closed or encrypted")
pdf = _as_pdf_document(self)
ASSERT_PDF(pdf)
mupdf.pdf_end_operation(pdf)
def journal_undo(self):
"""Move backwards in the journal."""
if self.is_closed or self.is_encrypted:
raise ValueError("document closed or encrypted")
pdf = _as_pdf_document(self)
ASSERT_PDF(pdf)
mupdf.pdf_undo(pdf)
return True
@property
def language(self):
"""Document language."""
if self.is_closed:
raise ValueError("document closed")
pdf = _as_pdf_document(self)
if not pdf:
return
lang = mupdf.pdf_document_language(pdf)
if lang == mupdf.FZ_LANG_UNSET:
return
assert 0, 'not implemented yet'
@property
def last_location(self):
"""Id (chapter, page) of last page."""
if self.is_closed:
raise ValueError("document closed")
last_loc = mupdf.fz_last_page(self.this)
return last_loc.chapter, last_loc.page
def layer_ui_configs(self):
"""Show OC visibility status modifiable by user."""
if self.is_closed:
raise ValueError("document closed")
pdf = _as_pdf_document(self)
ASSERT_PDF(pdf)
info = mupdf.PdfLayerConfigUi()
n = mupdf.pdf_count_layer_config_ui( pdf)
rc = []
for i in range(n):
mupdf.pdf_layer_config_ui_info( pdf, i, info)
if info.type == 1:
type_ = "checkbox"
elif info.type == 2:
type_ = "radiobox"
else:
type_ = "label"
item = {
"number": i,
"text": info.text,
"depth": info.depth,
"type": type_,
"on": info.selected,
"locked": info.locked,
}
rc.append(item)
return rc
def layout(self, rect=None, width=0, height=0, fontsize=11):
"""Re-layout a reflowable document."""
if self.is_closed or self.isEncrypted:
raise ValueError("document closed or encrypted")
doc = self.this
if not mupdf.fz_is_document_reflowable( doc):
return
w = width
h = height
r = JM_rect_from_py(rect)
if not mupdf.fz_is_infinite_rect(r):
w = r.x1 - r.x0
h = r.y1 - r.y0
if w <= 0.0 or h <= 0.0:
raise ValueError( "bad page size")
mupdf.fz_layout_document( doc, w, h, fontsize)
self._reset_page_refs()
self.init_doc()
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.isEncrypted:
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
if isinstance(page_id, int):
page = mupdf.fz_load_page(self.this, page_id)
else:
chapter, pagenum = page_id
page = mupdf.fz_load_chapter_page(self.this, chapter, pagenum)
val = Page(page, self)
val.thisown = True
val.parent = self
self._page_refs[id(val)] = val
val._annot_refs = weakref.WeakValueDictionary()
val.number = page_id
return val
def location_from_page_number(self, pno):
"""Convert pno to (chapter, page)."""
if self.is_closed:
raise ValueError("document closed")
this_doc = self.this
loc = mupdf.fz_make_location(-1, -1)
page_count = mupdf.fz_count_pages(this_doc)
while pno < 0:
pno += page_count
if pno >= page_count:
raise ValueError( MSG_BAD_PAGENO)
loc = mupdf.fz_location_from_page_number(this_doc, pno)
return loc.chapter, loc.page
def make_bookmark(self, loc):
"""Make a page pointer before layouting document."""
if self.is_closed or self.isEncrypted:
raise ValueError("document closed or encrypted")
loc = mupdf.FzLocation(*loc)
mark = mupdf.ll_fz_make_bookmark2( self.this.m_internal, loc.internal())
return mark
@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 is 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 Exception:
return valid
if value == "true":
valid[key] = True
return valid
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 need_appearances(self, value=None):
"""Get/set the NeedAppearances value."""
if self.is_closed:
raise ValueError("document closed")
if not self.isFormPDF:
return None
pdf = _as_pdf_document(self)
oldval = -1
appkey = "NeedAppearances"
form = mupdf.pdf_dict_getp(
mupdf.pdf_trailer(pdf),
"Root/AcroForm",
)
app = mupdf.pdf_dict_gets(form, appkey)
if mupdf.pdf_is_bool(app):
oldval = mupdf.pdf_to_bool(app)
if value:
mupdf.pdf_dict_puts(form, appkey, mupdf.PDF_TRUE)
else:
mupdf.pdf_dict_puts(form, appkey, mupdf.PDF_FALSE)
if value is None:
return oldval >= 0
return value
@property
def needs_pass(self):
"""Indicate password required."""
if self.is_closed:
raise ValueError("document closed")
document = self.this if isinstance(self.this, mupdf.FzDocument) else self.this.super()
ret = mupdf.fz_needs_password( document)
return ret
def next_location(self, page_id):
"""Get (chapter, page) of next page."""
if self.is_closed or self.isEncrypted:
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 ()
this_doc = _as_fz_document(self)
val = page_id[ 0]
if not isinstance(val, int):
RAISEPY(MSG_BAD_PAGEID, PyExc_ValueError)
chapter = val
val = page_id[ 1]
pno = val
loc = mupdf.fz_make_location(chapter, pno)
next_loc = mupdf.fz_next_page( this_doc, loc)
return next_loc.chapter, next_loc.page
def page_annot_xrefs(self, n):
if g_use_extra:
return extra.page_annot_xrefs( self.this, n)
if isinstance(self.this, mupdf.PdfDocument):
page_count = mupdf.pdf_count_pages(self.this)
pdf_document = self.this
else:
page_count = mupdf.fz_count_pages(self.this)
pdf_document = _as_pdf_document(self)
while n < 0:
n += page_count
if n > page_count:
raise ValueError( MSG_BAD_PAGENO)
page_obj = mupdf.pdf_lookup_page_obj(pdf_document, n)
annots = JM_get_annot_xref_list(page_obj)
return annots
@property
def page_count(self):
"""Number of pages."""
if g_use_extra:
return self.page_count2(self)
if isinstance( self.this, mupdf.FzDocument):
return mupdf.fz_count_pages( self.this)
else:
return mupdf.pdf_count_pages( self.this)
def page_cropbox(self, pno):
"""Get CropBox of page number (without loading page)."""
if self.is_closed:
raise ValueError("document closed")
this_doc = self.this
page_count = mupdf.fz_count_pages( this_doc)
n = pno
while n < 0:
n += page_count
pdf = _as_pdf_document(self)
if n >= page_count:
raise ValueError( MSG_BAD_PAGENO)
ASSERT_PDF(pdf)
pageref = mupdf.pdf_lookup_page_obj( pdf, n)
cropbox = JM_cropbox(pageref)
val = JM_py_from_rect(cropbox)
val = Rect(val)
return val
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")
chapter, pno = page_id
loc = mupdf.fz_make_location( chapter, pno)
page_n = mupdf.fz_page_number_from_location( self.this, loc)
return page_n
def page_xref(self, pno):
"""Get xref of page number."""
if g_use_extra:
return extra.page_xref( self.this, pno)
if self.is_closed:
raise ValueError("document closed")
page_count = mupdf.fz_count_pages(self.this)
n = pno
while n < 0:
n += page_count
pdf = _as_pdf_document(self)
xref = 0
if n >= page_count:
raise ValueError( MSG_BAD_PAGENO)
ASSERT_PDF(pdf)
xref = mupdf.pdf_to_num(mupdf.pdf_lookup_page_obj(pdf, n))
return xref
@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"
@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 pages(self, start: OptInt =None, stop: OptInt =None, step: OptInt =None) -> Page:
"""Return a generator iterator over a page range.
Arguments have the same meaning as for the range() built-in.
"""
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")
stop = stop if stop is not None and stop <= self.page_count else self.page_count
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 pdf_catalog(self):
"""Get xref of PDF catalog."""
if self.is_closed:
raise ValueError("document closed")
pdf = _as_pdf_document(self)
xref = 0
if not pdf:
return xref
root = mupdf.pdf_dict_get(mupdf.pdf_trailer(pdf), PDF_NAME('Root'))
xref = mupdf.pdf_to_num(root)
return xref
def pdf_trailer(self, compressed=0, ascii=0):
"""Get PDF trailer as a string."""
return self.xref_object(-1, compressed=compressed, ascii=ascii)
@property
def permissions(self):
"""Document permissions."""
if self.isEncrypted:
return 0
doc =self.this
pdf = mupdf.pdf_document_from_fz_document(doc)
if pdf.m_internal:
return mupdf.pdf_document_permissions(pdf)
perm = 0xFFFFFFFC
if not mupdf.fz_has_permission(doc, mupdf.FZ_PERMISSION_PRINT):
perm = perm ^ mupdf.PDF_PERM_PRINT
if not mupdf.fz_has_permission(doc, mupdf.FZ_PERMISSION_EDIT):
perm = perm ^ mupdf.PDF_PERM_MODIFY
if not mupdf.fz_has_permission(doc, mupdf.FZ_PERMISSION_COPY):
perm = perm ^ mupdf.PDF_PERM_COPY
if not mupdf.fz_has_permission(doc, mupdf.FZ_PERMISSION_ANNOTATE):
perm = perm ^ mupdf.PDF_PERM_ANNOTATE
return perm
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 ()
chapter, pno = page_id
loc = mupdf.fz_make_location(chapter, pno)
prev_loc = mupdf.fz_previous_page(self.this, loc)
return prev_loc.chapter, prev_loc.page
def reload_page(self, page: Page) -> Page:
"""Make a fresh copy of a page."""
old_annots = {}
pno = page.number
for k, v in page._annot_refs.items():
old_annots[k] = v
assert page.this.m_internal.refs == 1
page.this = None
page._erase()
page = None
TOOLS.store_shrink(100)
page = self.load_page(pno)
for k, v in old_annots.items():
annot = old_annots[k]
page._annot_refs[k] = annot
return page
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).
"""
if not uri:
if chapters:
return (-1, -1), 0, 0
return -1, 0, 0
try:
loc, xp, yp = mupdf.fz_resolve_link(self.this, uri)
except Exception:
if g_exceptions_verbose: exception_info()
if chapters:
return (-1, -1), 0, 0
return -1, 0, 0
if chapters:
return (loc.chapter, loc.page), xp, yp
pno = mupdf.fz_page_number_from_location(self.this, loc)
return pno, xp, yp
def resolve_names(self):
"""Convert the PDF's destination names into a Python dict.
This function must be used with PyMuPDF's new, "rebased" architecture -
see the above import statement.
The only parameter is the fitz.Document.
All names found in the catalog under keys "/Dests" and "/Names/Dests" are
being included.
Returns:
A dcitionary with the following layout:
- key: (str) the name
- value: (dict) with the following layout:
* "page": target page number (0-based). If no page number found -1.
* "to": (x, y) target point on page - currently in PDF coordinates,
i.e. point (0,0) is the bottom-left of the page.
* "zoom": (float) the zoom factor
* "dest": (str) only occurs if the target location on the page has
not been provided as "/XYZ" or if no page number was found.
Examples:
{'__bookmark_1': {'page': 0, 'to': (0.0, 541.0), 'zoom': 0.0},
'__bookmark_2': {'page': 0, 'to': (0.0, 481.45), 'zoom': 0.0}}
or
'21154a7c20684ceb91f9c9adc3b677c40': {'page': -1, 'dest': '/XYZ 15.75 1486 0'}, ...
"""
page_xrefs = {self.page_xref(i): i for i in range(self.page_count)}
def obj_string(obj):
"""Return string version of a PDF object definition."""
buffer = mupdf.fz_new_buffer(512)
output = mupdf.FzOutput(buffer)
mupdf.pdf_print_obj(output, obj, 1, 0)
return JM_UnicodeFromBuffer(buffer)
def get_array(val):
"""Generate value of one item of the names dictionary."""
templ_dict = {"page": -1, "dest": ""}
if val.pdf_is_indirect():
val = mupdf.pdf_resolve_indirect(val)
if val.pdf_is_array():
array = obj_string(val)
elif val.pdf_is_dict():
array = obj_string(mupdf.pdf_dict_gets(val, "D"))
else:
return templ_dict
array = array.replace("null", "0")[1:-1]
idx = array.find("/")
if idx < 1:
templ_dict["dest"] = array
return templ_dict
subval = array[:idx]
array = array[idx:]
templ_dict["dest"] = array
if array.startswith("/XYZ"):
del templ_dict["dest"]
arr_t = array.split()[1:]
x, y, z = tuple(map(float, arr_t))
templ_dict["to"] = (x, y)
templ_dict["zoom"] = z
if "0 R" in subval:
templ_dict["page"] = page_xrefs[int(subval.split()[0])]
else:
templ_dict["page"] = int(subval)
return templ_dict
def fill_dict(dest_dict, pdf_dict):
"""Generate name resolution items for pdf_dict.
This may be either "/Names/Dests" or just "/Dests"
"""
name_count = mupdf.pdf_dict_len(pdf_dict)
for i in range(name_count):
key = mupdf.pdf_dict_get_key(pdf_dict, i)
val = mupdf.pdf_dict_get_val(pdf_dict, i)
if key.pdf_is_name():
dict_key = key.pdf_to_name()
else:
print(f"key {i} is no /Name")
dict_key = None
if dict_key:
dest_dict[dict_key] = get_array(val)
pdf = mupdf.pdf_document_from_fz_document(self)
catalog = mupdf.pdf_dict_gets(mupdf.pdf_trailer(pdf), "Root")
dest_dict = {}
dests = mupdf.pdf_new_name("Dests")
old_dests = mupdf.pdf_dict_get(catalog, dests)
if old_dests.pdf_is_dict():
fill_dict(dest_dict, old_dests)
tree = mupdf.pdf_load_name_tree(pdf, dests)
if tree.pdf_is_dict():
fill_dict(dest_dict, tree)
return dest_dict
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) is str:
pass
elif hasattr(filename, "open"):
filename = str(filename)
elif hasattr(filename, "name"):
filename = filename.name
elif not hasattr(filename, "seek"):
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")
if g_use_extra:
return extra.Document_save(
self.this,
filename,
garbage,
clean,
deflate,
deflate_images,
deflate_fonts,
incremental,
ascii,
expand,
linear,
no_new_id,
appearance,
pretty,
encryption,
permissions,
owner_pw,
user_pw,
)
pdf = _as_pdf_document(self)
opts = mupdf.PdfWriteOptions()
opts.do_incremental = incremental
opts.do_ascii = ascii
opts.do_compress = deflate
opts.do_compress_images = deflate_images
opts.do_compress_fonts = deflate_fonts
opts.do_decompress = expand
opts.do_garbage = garbage
opts.do_pretty = pretty
opts.do_linear = linear
opts.do_clean = clean
opts.do_sanitize = clean
opts.dont_regenerate_id = no_new_id
opts.do_appearance = appearance
opts.do_encrypt = encryption
opts.permissions = permissions
if owner_pw is not None:
opts.opwd_utf8_set_value(owner_pw)
elif user_pw is not None:
opts.opwd_utf8_set_value(user_pw)
if user_pw is not None:
opts.upwd_utf8_set_value(user_pw)
out = None
ASSERT_PDF(pdf)
pdf.m_internal.resynth_required = 0
JM_embedded_clean(pdf)
if no_new_id == 0:
JM_ensure_identity(pdf)
if isinstance(filename, str):
mupdf.pdf_save_document(pdf, filename, opts)
else:
out = JM_new_output_fileptr(filename)
mupdf.pdf_write_document(pdf, out, opts)
def save_snapshot(self, filename):
"""Save a file snapshot suitable for journalling."""
if self.is_closed:
raise ValueError("doc is closed")
if type(filename) is str:
pass
elif hasattr(filename, "open"):
filename = str(filename)
elif hasattr(filename, "name"):
filename = filename.name
else:
raise ValueError("filename must be str, Path or file object")
if filename == self.name:
raise ValueError("cannot snapshot to original")
pdf = _as_pdf_document(self)
ASSERT_PDF(pdf)
mupdf.pdf_save_snapshot(pdf, filename)
def saveIncr(self):
""" Save PDF incrementally"""
return self.save(self.name, incremental=True, encryption=mupdf.PDF_ENCRYPT_KEEP)
def select(self, pyliste):
"""Build sub-pdf with page numbers in the list."""
if self.is_closed or self.isEncrypted:
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)")
pdf = _as_pdf_document(self)
retainpages(pdf, pyliste)
if pdf.m_internal.rev_page_map:
mupdf.ll_pdf_drop_page_tree(pdf.m_internal)
self._reset_page_refs()
def set_language(self, language=None):
pdf = _as_pdf_document(self)
ASSERT_PDF(pdf)
if not language:
lang = mupdf.FZ_LANG_UNSET
else:
lang = mupdf.fz_text_language_from_string(language)
mupdf.pdf_set_document_language(pdf, lang)
return True
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'")
pdf = _as_pdf_document(self)
ASSERT_PDF(pdf)
ocp = mupdf.pdf_dict_getl(
mupdf.pdf_trailer( pdf),
PDF_NAME('Root'),
PDF_NAME('OCProperties'),
)
if not ocp.m_internal:
return
if config == -1:
obj = mupdf.pdf_dict_get( ocp, PDF_NAME('D'))
else:
obj = mupdf.pdf_array_get(
mupdf.pdf_dict_get( ocp, PDF_NAME('Configs')),
config,
)
if not obj.m_internal:
raise ValueError( MSG_BAD_OC_CONFIG)
JM_set_ocg_arrays( obj, basestate, on, off, rbgroups, locked)
mupdf.ll_pdf_read_ocg( pdf.m_internal)
def set_layer_ui_config(self, number, action=0):
"""Set / unset OC intent configuration."""
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]
pdf = _as_pdf_document(self)
assert pdf
if action == 1:
mupdf.pdf_toggle_layer_config_ui(pdf, number)
elif action == 2:
mupdf.pdf_deselect_layer_config_ui(pdf, number)
else:
mupdf.pdf_select_layer_config_ui(pdf, number)
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 value not 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 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")
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")
def set_xml_metadata(self, metadata):
"""Store XML document level metadata."""
if self.is_closed or self.isEncrypted:
raise ValueError("document closed or encrypted")
pdf = _as_pdf_document(self)
ASSERT_PDF(pdf)
root = mupdf.pdf_dict_get( mupdf.pdf_trailer( pdf), PDF_NAME('Root'))
if not root.m_internal:
RAISEPY( MSG_BAD_PDFROOT, JM_Exc_FileDataError)
res = mupdf.fz_new_buffer_from_copied_data( metadata.encode('utf-8'))
xml = mupdf.pdf_dict_get( root, PDF_NAME('Metadata'))
if xml.m_internal:
JM_update_stream( pdf, xml, res, 0)
else:
xml = mupdf.pdf_add_stream( pdf, res, None, 0)
mupdf.pdf_dict_put( xml, PDF_NAME('Type'), PDF_NAME('Metadata'))
mupdf.pdf_dict_put( xml, PDF_NAME('Subtype'), PDF_NAME('XML'))
mupdf.pdf_dict_put( root, PDF_NAME('Metadata'), xml)
def switch_layer(self, config, as_default=0):
"""Activate an OC layer."""
if self.is_closed:
raise ValueError("document closed")
pdf = _as_pdf_document(self)
ASSERT_PDF(pdf)
cfgs = mupdf.pdf_dict_getl(
mupdf.pdf_trailer( pdf),
PDF_NAME('Root'),
PDF_NAME('OCProperties'),
PDF_NAME('Configs')
)
if not mupdf.pdf_is_array( cfgs) or not mupdf.pdf_array_len( cfgs):
if config < 1:
return
raise ValueError( MSG_BAD_OC_LAYER)
if config < 0:
return
mupdf.pdf_select_layer_config( pdf, config)
if as_default:
mupdf.pdf_set_layer_config_as_default( pdf)
mupdf.ll_pdf_read_ocg( pdf.m_internal)
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")
pdf = _as_pdf_document(self)
ASSERT_PDF(pdf)
xreflen = mupdf.pdf_xref_len(pdf)
if not _INRANGE(xref, 1, xreflen-1):
RAISEPY("bad xref", MSG_BAD_XREF, PyExc_ValueError)
ENSURE_OPERATION(pdf)
new_obj = JM_pdf_obj_from_str(pdf, text)
mupdf.pdf_update_object(pdf, xref, new_obj)
if page:
JM_refresh_links( mupdf.pdf_page_from_fz_page(page.super()))
def update_stream(self, xref=0, stream=None, new=1, compress=1):
"""Replace xref stream part."""
if self.is_closed or self.isEncrypted:
raise ValueError("document closed or encrypted")
pdf = _as_pdf_document(self)
xreflen = mupdf.pdf_xref_len(pdf)
if xref < 1 or xref > xreflen:
raise ValueError( MSG_BAD_XREF)
obj = mupdf.pdf_new_indirect(pdf, xref, 0)
if not mupdf.pdf_is_dict(obj):
raise ValueError( MSG_IS_NO_DICT)
res = JM_BufferFromBytes(stream)
if not res:
raise TypeError( MSG_BAD_BUFFER)
JM_update_stream(pdf, obj, res, compress)
pdf.dirty = 1
@property
def version_count(self):
'''
Count versions of PDF document.
'''
pdf = _as_pdf_document(self)
if pdf.m_internal:
return mupdf.pdf_count_versions(pdf)
return 0
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()
@property
def xref(self):
"""PDF xref number of page."""
CheckParent(self)
return self.parent.page_xref(self.number)
def xref_get_key(self, xref, key):
"""Get PDF dict key value of object at 'xref'."""
if self.is_closed:
raise ValueError("document closed")
pdf = _as_pdf_document(self)
ASSERT_PDF(pdf)
xreflen = mupdf.pdf_xref_len(pdf)
if not _INRANGE(xref, 1, xreflen-1) and xref != -1:
raise ValueError( MSG_BAD_XREF)
if xref > 0:
obj = mupdf.pdf_load_object(pdf, xref)
else:
obj = mupdf.pdf_trailer(pdf)
if not obj.m_internal:
return ("null", "null")
subobj = mupdf.pdf_dict_getp(obj, key)
if not subobj.m_internal:
return ("null", "null")
text = None
if mupdf.pdf_is_indirect(subobj):
type = "xref"
text = "%i 0 R" % mupdf.pdf_to_num(subobj)
elif mupdf.pdf_is_array(subobj):
type = "array"
elif mupdf.pdf_is_dict(subobj):
type = "dict"
elif mupdf.pdf_is_int(subobj):
type = "int"
text = "%i" % mupdf.pdf_to_int(subobj)
elif mupdf.pdf_is_real(subobj):
type = "float"
elif mupdf.pdf_is_null(subobj):
type = "null"
text = "null"
elif mupdf.pdf_is_bool(subobj):
type = "bool"
if mupdf.pdf_to_bool(subobj):
text = "true"
else:
text = "false"
elif mupdf.pdf_is_name(subobj):
type = "name"
text = "/%s" % mupdf.pdf_to_name(subobj)
elif mupdf.pdf_is_string(subobj):
type = "string"
text = JM_UnicodeFromStr(mupdf.pdf_to_text_string(subobj))
else:
type = "unknown"
if text is None:
res = JM_object_to_buffer(subobj, 1, 0)
text = JM_UnicodeFromBuffer(res)
return (type, text)
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")
pdf = _as_pdf_document(self)
ASSERT_PDF(pdf)
xreflen = mupdf.pdf_xref_len( pdf)
if not _INRANGE(xref, 1, xreflen-1) and xref != -1:
raise ValueError( MSG_BAD_XREF)
if xref > 0:
obj = mupdf.pdf_load_object( pdf, xref)
else:
obj = mupdf.pdf_trailer( pdf)
n = mupdf.pdf_dict_len( obj)
rc = []
if n == 0:
return rc
for i in range(n):
key = mupdf.pdf_to_name( mupdf.pdf_dict_get_key( obj, i))
rc.append(key)
return rc
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_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_stream(self, xref=0):
"""Check if xref is a stream object."""
if self.is_closed:
raise ValueError("document closed")
pdf = _as_pdf_document(self)
if not pdf:
return False
return bool(mupdf.pdf_obj_num_is_stream(pdf, xref))
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 xref_length(self):
"""Get length of xref table."""
if self.is_closed:
raise ValueError("document closed")
xreflen = 0
pdf = _as_pdf_document(self)
if pdf:
xreflen = mupdf.pdf_xref_len(pdf)
return xreflen
def xref_object(self, xref, compressed=0, ascii=0):
"""Get xref object source as a string."""
if self.is_closed:
raise ValueError("document closed")
if g_use_extra:
ret = extra.xref_object( self.this, xref, compressed, ascii)
return ret
pdf = _as_pdf_document(self)
ASSERT_PDF(pdf)
xreflen = mupdf.pdf_xref_len(pdf)
if not _INRANGE(xref, 1, xreflen-1) and xref != -1:
raise ValueError( MSG_BAD_XREF)
if xref > 0:
obj = mupdf.pdf_load_object(pdf, xref)
else:
obj = mupdf.pdf_trailer(pdf)
res = JM_object_to_buffer(mupdf.pdf_resolve_indirect(obj), compressed, ascii)
text = JM_EscapeStrFromBuffer(res)
return text
def xref_set_key(self, xref, key, value):
"""Set the value of a PDF dictionary key."""
if self.is_closed:
raise ValueError("document closed")
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'")
pdf = _as_pdf_document(self)
ASSERT_PDF(pdf)
xreflen = mupdf.pdf_xref_len(pdf)
if not _INRANGE(xref, 1, xreflen-1) and xref != -1:
raise ValueError( MSG_BAD_XREF)
if xref != -1:
obj = mupdf.pdf_load_object(pdf, xref)
else:
obj = mupdf.pdf_trailer(pdf)
new_obj = JM_set_object_value(obj, key, value)
if not new_obj.m_internal:
return
if xref != -1:
mupdf.pdf_update_object(pdf, xref, new_obj)
else:
n = mupdf.pdf_dict_len(new_obj)
for i in range(n):
mupdf.pdf_dict_put(
obj,
mupdf.pdf_dict_get_key(new_obj, i),
mupdf.pdf_dict_get_val(new_obj, i),
)
def xref_stream(self, xref):
"""Get decompressed xref stream."""
if self.is_closed or self.isEncrypted:
raise ValueError("document closed or encrypted")
pdf = _as_pdf_document(self)
ASSERT_PDF(pdf)
xreflen = mupdf.pdf_xref_len( pdf)
if not _INRANGE(xref, 1, xreflen-1) and xref != -1:
raise ValueError( MSG_BAD_XREF)
if xref >= 0:
obj = mupdf.pdf_new_indirect( pdf, xref, 0)
else:
obj = mupdf.pdf_trailer( pdf)
r = None
if mupdf.pdf_is_stream( obj):
res = mupdf.pdf_load_stream_number( pdf, xref)
r = JM_BinFromBuffer( res)
return r
def xref_stream_raw(self, xref):
"""Get xref stream without decompression."""
if self.is_closed or self.isEncrypted:
raise ValueError("document closed or encrypted")
pdf = _as_pdf_document(self)
ASSERT_PDF(pdf)
xreflen = mupdf.pdf_xref_len( pdf)
if not _INRANGE(xref, 1, xreflen-1) and xref != -1:
raise ValueError( MSG_BAD_XREF)
if xref >= 0:
obj = mupdf.pdf_new_indirect( pdf, xref, 0)
else:
obj = mupdf.pdf_trailer( pdf)
r = None
if mupdf.pdf_is_stream( obj):
res = mupdf.pdf_load_raw_stream_number( pdf, xref)
r = JM_BinFromBuffer( res)
return r
def xref_xml_metadata(self):
"""Get xref of document XML metadata."""
if self.is_closed:
raise ValueError("document closed")
pdf = _as_pdf_document(self)
ASSERT_PDF(pdf)
root = mupdf.pdf_dict_get( mupdf.pdf_trailer( pdf), PDF_NAME('Root'))
if not root.m_internal:
RAISEPY( MSG_BAD_PDFROOT, JM_Exc_FileDataError)
xml = mupdf.pdf_dict_get( root, PDF_NAME('Metadata'))
xref = 0
if xml.m_internal:
xref = mupdf.pdf_to_num( xml)
return xref
if mupdf_version_tuple < (1, 22):
@property
def has_old_style_xrefs(self):
'''
Check if xref table is old style.
'''
if self.is_closed:
raise ValueError("document closed")
pdf = _as_pdf_document(self)
if pdf.m_internal and pdf.m_internal.has_old_style_xrefs:
return True
return False
@property
def has_xref_streams(self):
'''
Check if xref table is a stream.
'''
if self.is_closed:
raise ValueError("document closed")
pdf = _as_pdf_document(self)
if pdf.m_internal and pdf.m_internal.has_xref_streams:
return True
return False
__slots__ = ('this', 'page_count2', 'this_is_pdf', '__dict__')
outline = property(lambda self: self._outline)
tobytes = write
is_stream = xref_is_stream
open = Document
class DocumentWriter:
def __enter__(self):
return self
def __exit__(self, *args):
self.close()
def __init__(self, path, options=''):
if isinstance( path, str):
pass
elif hasattr( path, 'absolute'):
path = str( path)
elif hasattr( path, 'name'):
path = path.name
if isinstance( path, str):
self.this = mupdf.FzDocumentWriter( path, options, mupdf.FzDocumentWriter.PathType_PDF)
else:
out = JM_new_output_fileptr( path)
self.this = mupdf.FzDocumentWriter( out, options, mupdf.FzDocumentWriter.OutputType_PDF)
assert out.m_internal_value() == 0
assert hasattr( self.this, '_out')
def begin_page( self, mediabox):
mediabox2 = JM_rect_from_py(mediabox)
device = mupdf.fz_begin_page( self.this, mediabox2)
device_wrapper = DeviceWrapper( device)
return device_wrapper
def close( self):
mupdf.fz_close_document_writer( self.this)
def end_page( self):
mupdf.fz_end_page( self.this)
class Font:
def __del__(self):
if type(self) is not Font:
return None
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
fontbuffer = pymupdf_fonts.myfont(fname_lower)
fontname = None
del pymupdf_fonts
elif ordering < 0:
fontname = Base14_fontdict.get(fontname, fontname)
lang = mupdf.fz_text_language_from_string(language)
font = JM_get_font(fontname, fontfile,
fontbuffer, script, lang, ordering,
is_bold, is_italic, is_serif, embed)
self.this = font
def __repr__(self):
return "Font('%s')" % self.name
def _valid_unicodes(self, arr):
assert 0, 'Not implemented because implementation requires FT_Get_First_Char() etc.'
@property
def ascender(self):
"""Return the glyph ascender value."""
return mupdf.fz_font_ascender(self.this)
@property
def buffer(self):
buffer_ = mupdf.FzBuffer( mupdf.ll_fz_keep_buffer( self.this.m_internal.buffer))
return mupdf.fz_buffer_extract_copy( buffer_)
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."""
lang = mupdf.fz_text_language_from_string(language)
rc = []
for ch in text:
c = ord(ch)
if small_caps:
gid = mupdf.fz_encode_character_sc(self.this, c)
if gid >= 0:
font = self.this
else:
gid, font = mupdf.fz_encode_character_with_fallback(self.this, c, script, lang)
rc.append(fontsize * mupdf.fz_advance_glyph(font, gid, wmode))
return rc
@property
def descender(self):
"""Return the glyph descender value."""
return mupdf.fz_font_descender(self.this)
@property
def flags(self):
f = mupdf.ll_fz_font_flags(self.this.m_internal)
if not f:
return
assert isinstance( f, mupdf.fz_font_flags_t)
if mupdf_cppyy:
v = [f.is_mono]
def b(bits):
ret = v[0] & ((1 << bits)-1)
v[0] = v[0] >> bits
return ret
is_mono = b(1)
is_serif = b(1)
is_bold = b(1)
is_italic = b(1)
ft_substitute = b(1)
ft_stretch = b(1)
fake_bold = b(1)
fake_italic = b(1)
has_opentype = b(1)
invalid_bbox = b(1)
cjk_lang = b(1)
embed = b(1)
never_embed = b(1)
return {
"mono": is_mono if mupdf_cppyy else f.is_mono,
"serif": is_serif if mupdf_cppyy else f.is_serif,
"bold": is_bold if mupdf_cppyy else f.is_bold,
"italic": is_italic if mupdf_cppyy else f.is_italic,
"substitute": ft_substitute if mupdf_cppyy else f.ft_substitute,
"stretch": ft_stretch if mupdf_cppyy else f.ft_stretch,
"fake-bold": fake_bold if mupdf_cppyy else f.fake_bold,
"fake-italic": fake_italic if mupdf_cppyy else f.fake_italic,
"opentype": has_opentype if mupdf_cppyy else f.has_opentype,
"invalid-bbox": invalid_bbox if mupdf_cppyy else f.invalid_bbox,
'cjk': cjk_lang if mupdf_cppyy else f.cjk,
'cjk-lang': cjk_lang if mupdf_cppyy else f.cjk_lang,
'embed': embed if mupdf_cppyy else f.embed,
'never-embed': never_embed if mupdf_cppyy else f.never_embed,
}
def glyph_advance(self, chr_, language=None, script=0, wmode=0, small_caps=0):
"""Return the glyph width of a unicode (font size 1)."""
lang = mupdf.fz_text_language_from_string(language)
if small_caps:
gid = mupdf.fz_encode_character_sc(self.this, chr_)
if gid >= 0:
font = self.this
else:
gid, font = mupdf.fz_encode_character_with_fallback(self.this, chr_, script, lang)
return mupdf.fz_advance_glyph(font, gid, wmode)
def glyph_bbox(self, char, language=None, script=0, small_caps=0):
"""Return the glyph bbox of a unicode (font size 1)."""
lang = mupdf.fz_text_language_from_string(language)
if small_caps:
gid = mupdf.fz_encode_character_sc( self.this, char)
if gid >= 0:
font = self.this
else:
gid, font = mupdf.fz_encode_character_with_fallback( self.this, char, script, lang)
return Rect(mupdf.fz_bound_glyph( font, gid, mupdf.FzMatrix()))
@property
def glyph_count(self):
return self.this.m_internal.glyph_count
def glyph_name_to_unicode(self, name):
"""Return the unicode for a glyph name."""
return glyph_name_to_unicode(name)
def has_glyph(self, chr, language=None, script=0, fallback=0, small_caps=0):
"""Check whether font has a glyph for this unicode."""
if fallback:
lang = mupdf.fz_text_language_from_string(language)
gid, font = mupdf.fz_encode_character_with_fallback(self.this, chr, script, lang)
else:
if small_caps:
gid = mupdf.fz_encode_character_sc(self.this, chr)
else:
gid = mupdf.fz_encode_character(self.this, chr)
return gid
@property
def is_bold(self):
return mupdf.fz_font_is_bold( self.this)
@property
def is_italic(self):
return mupdf.fz_font_is_italic( self.this)
@property
def is_monospaced(self):
return mupdf.fz_font_is_monospaced( self.this)
@property
def is_serif(self):
return mupdf.fz_font_is_serif( self.this)
@property
def is_writable(self):
return True
font = self.this
flags = mupdf.ll_fz_font_flags(font.m_internal)
if mupdf_cppyy:
import cppyy
ft_substitute = cppyy.gbl.mupdf_mfz_font_flags_ft_substitute( flags)
else:
ft_substitute = flags.ft_substitute
if ( mupdf.ll_fz_font_t3_procs(font.m_internal)
or ft_substitute
or not mupdf.pdf_font_writing_supported(font)
):
return False
return True
@property
def name(self):
ret = mupdf.fz_font_name(self.this)
return ret
def text_length(self, text, fontsize=11, language=None, script=0, wmode=0, small_caps=0):
"""Return length of unicode 'text' under a fontsize."""
thisfont = self.this
lang = mupdf.fz_text_language_from_string(language)
rc = 0
if not isinstance(text, str):
raise TypeError( MSG_BAD_TEXT)
for ch in text:
c = ord(ch)
if small_caps:
gid = mupdf.fz_encode_character_sc(thisfont, c)
if gid >= 0:
font = thisfont
else:
gid, font = mupdf.fz_encode_character_with_fallback(thisfont, c, script, lang)
rc += mupdf.fz_advance_glyph(font, gid, wmode)
rc *= fontsize
return rc
def unicode_to_glyph_name(self, ch):
"""Return the glyph name for a unicode."""
return unicode_to_glyph_name(ch)
def valid_codepoints(self):
'''
list of valid unicodes of a fz_font
'''
return []
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:])
class Graftmap:
def __del__(self):
if not type(self) is Graftmap:
return
self.thisown = False
def __init__(self, doc):
dst = doc._this_as_pdf_document()
ASSERT_PDF(dst)
map_ = mupdf.pdf_new_graft_map(dst)
self.this = map_
self.thisown = True
class Link:
def __del__(self):
self._erase()
def __init__( self, this):
assert isinstance( this, mupdf.FzLink)
self.this = this
def __repr__(self):
CheckParent(self)
return "link on " + str(self.parent)
def __str__(self):
CheckParent(self)
return "link on " + str(self.parent)
def _border(self, doc, xref):
pdf = _as_pdf_document(doc)
if not pdf:
return
link_obj = mupdf.pdf_new_indirect(pdf, xref, 0)
if not link_obj.m_internal:
return
b = JM_annot_border(link_obj)
return b
def _colors(self, doc, xref):
pdf = _as_pdf_document(doc)
if not pdf:
return
link_obj = mupdf.pdf_new_indirect( pdf, xref, 0)
if not link_obj.m_internal:
raise ValueError( MSG_BAD_XREF)
b = JM_annot_colors( link_obj)
return b
def _erase(self):
self.parent = None
self.thisown = False
def _setBorder(self, border, doc, xref):
pdf = _as_pdf_document(doc)
if not pdf:
return
link_obj = mupdf.pdf_new_indirect(pdf, xref, 0)
if not link_obj.m_internal:
return
b = JM_annot_set_border(border, pdf, link_obj)
return b
@property
def border(self):
return self._border(self.parent.parent.this, self.xref)
@property
def colors(self):
return self._colors(self.parent.parent.this, self.xref)
@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.isEncrypted:
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, doc)
@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
@property
def is_external(self):
"""Flag the link as external."""
CheckParent(self)
if g_use_extra:
return extra.Link_is_external( self.this)
this_link = self.this
if not this_link.m_internal or not this_link.m_internal.uri:
return False
return bool( mupdf.fz_is_external_link( this_link.m_internal.uri))
@property
def next(self):
"""Next link."""
if not self.this.m_internal:
return None
CheckParent(self)
if 0 and g_use_extra:
val = extra.Link_next( self.this)
else:
val = self.this.next()
if not val.m_internal:
return None
val = Link( val)
if val:
val.thisown = True
val.parent = self.parent
val.parent._annot_refs[id(val)] = val
if self.xref > 0:
link_xrefs = [x[0] for x in self.parent.annot_xrefs() if x[1] == mupdf.PDF_ANNOT_LINK]
link_ids = [x[2] for x in self.parent.annot_xrefs() if x[1] == mupdf.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
@property
def rect(self):
"""Rectangle ('hot area')."""
CheckParent(self)
if not self.this or not self.this.m_internal:
raise Exception( 'self.this.m_internal not available')
assert self.this
assert self.this.m_internal
val = JM_py_from_rect( self.this.rect())
val = Rect(val)
return val
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)
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)
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
@property
def uri(self):
"""Uri string."""
if g_use_extra:
return extra.link_uri(self.this)
this_link = self.this
return this_link.m_internal.uri if this_link.m_internal else ''
page = -1
class Matrix:
def __abs__(self):
return math.sqrt(sum([c*c for c in self]))
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 __bool__(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 __getitem__(self, i):
return (self.a, self.b, self.c, self.d, self.e, self.f)[i]
def __init__(self, *args, a=None, b=None, c=None, d=None, e=None, f=None):
"""
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'
Explicit keyword args a, b, c, d, e, f override any earlier settings if
not None.
"""
if not args:
self.a = self.b = self.c = self.d = self.e = self.f = 0.0
elif len(args) > 6:
raise ValueError("Matrix: bad seq len")
elif len(args) == 6:
self.a, self.b, self.c, self.d, self.e, self.f = map(float, args)
elif len(args) == 1:
if hasattr(args[0], "__float__"):
theta = math.radians(args[0])
c_ = round(math.cos(theta), 8)
s_ = round(math.sin(theta), 8)
self.a = self.d = c_
self.b = s_
self.c = -s_
self.e = self.f = 0.0
else:
self.a, self.b, self.c, self.d, self.e, self.f = map(float, args[0])
elif 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
elif 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
else:
raise ValueError("Matrix: bad args")
if a is not None: self.a = a
if b is not None: self.b = b
if c is not None: self.c = c
if d is not None: self.d = d
if e is not None: self.e = e
if f is not None: self.f = f
def __invert__(self):
"""Calculate inverted matrix."""
m1 = Matrix()
m1.invert(self)
return m1
def __len__(self):
return 6
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 __neg__(self):
return Matrix(-self.a, -self.b, -self.c, -self.d, -self.e, -self.f)
def __nonzero__(self):
return not (max(self) == min(self) == 0)
def __pos__(self):
return Matrix(self)
def __repr__(self):
return "Matrix" + str(tuple(self))
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 __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 __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)
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 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
@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)
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 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 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
__inv__ = __invert__
__div__ = __truediv__
norm = __abs__
class IdentityMatrix(Matrix):
"""Identity matrix [1, 0, 0, 1, 0, 0]"""
def __hash__(self):
return hash((1,0,0,1,0,0))
def __init__(self):
Matrix.__init__(self, 1.0, 1.0)
def __repr__(self):
return "IdentityMatrix(1.0, 0.0, 0.0, 1.0, 0.0, 0.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")
Identity = IdentityMatrix()
class linkDest:
"""link or outline destination details"""
def __init__(self, obj, rlink, document=None):
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
m = re.match('^#nameddest=(.*)', self.uri)
assert document
if document and m:
named = m.group(1)
self.named = document.resolve_names().get(named)
if self.named is None:
self.named = dict()
self.named['nameddest'] = named
else:
self.named = self.uri[1:]
else:
self.kind = LINK_NAMED
self.named = self.uri
if obj.is_external:
if not self.uri:
pass
elif 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
class Widget:
'''
Class describing a PDF form field ("widget")
'''
def __init__(self):
self.border_color = None
self.border_style = "S"
self.border_width = 0
self.border_dashes = None
self.choice_values = None
self.rb_parent = None
self.field_name = None
self.field_label = None
self.field_value = None
self.field_flags = 0
self.field_display = 0
self.field_type = 0
self.field_type_string = None
self.fill_color = None
self.button_caption = None
self.is_signed = None
self.text_color = (0, 0, 0)
self.text_font = "Helv"
self.text_fontsize = 0
self.text_maxlen = 0
self.text_format = 0
self._text_da = ""
self.script = None
self.script_stroke = None
self.script_format = None
self.script_change = None
self.script_calc = None
self.script_blur = None
self.script_focus = None
self.rect = None
self.xref = 0
def __repr__(self):
return f'Widget:(field_type={self.field_type_string} script={self.script})'
return "'%s' widget" % (self.field_type_string)
def _adjust_font(self):
"""Ensure text_font is from our list and correctly spelled.
"""
if not self.text_font:
self.text_font = "Helv"
return
valid_fonts = ("Cour", "TiRo", "Helv", "ZaDb")
for f in valid_fonts:
if self.text_font.lower() == f.lower():
self.text_font = f
return
self.text_font = "Helv"
return
def _checker(self):
"""Any widget type checks.
"""
if self.field_type not in range(1, 8):
raise ValueError("bad field type")
if self.field_type == mupdf.PDF_WIDGET_TYPE_RADIOBUTTON and self.field_value not in (False, "Off") and hasattr(self, "parent"):
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")
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()
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":
col = [(float(dat[i - 1]))]
dat[i] = dat[i-1] = ""
continue
if item == "rg":
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 _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]
btn_type = self.field_type in (
mupdf.PDF_WIDGET_TYPE_BUTTON,
mupdf.PDF_WIDGET_TYPE_CHECKBOX,
mupdf.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")
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()
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
if hasattr(self, "parent"):
doc = self.parent.parent
else:
return
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
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
return states
@property
def next(self):
return self._annot.next
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
if self.field_type == 2:
return "Yes"
bstate = self.button_states()
if bstate is None:
bstate = dict()
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 update(self):
"""Reflect Python object in the PDF.
"""
self._validate()
self._adjust_font()
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 self.script_calc:
util_ensure_widget_calc(self._annot)
TOOLS._save_widget(self._annot, self)
self._text_da = ""
from . import _extra
class Outline:
def __init__(self, ol):
self.this = ol
@property
def dest(self):
'''outline destination details'''
return linkDest(self, None, None)
def destination(self, document):
'''
Like `dest` property but uses `document` to resolve destinations for
kind=LINK_NAMED.
'''
return linkDest(self, None, document)
@property
def down(self):
ol = self.this
down_ol = ol.down()
if not down_ol.m_internal:
return
return Outline(down_ol)
@property
def is_external(self):
if g_use_extra:
return _extra.Outline_is_external( self.this)
ol = self.this
if not ol.m_internal:
return False
uri = ol.m_internal.uri if 1 else ol.uri()
if uri is None:
return False
return mupdf.fz_is_external_link(uri)
@property
def is_open(self):
if 1:
return self.this.m_internal.is_open
return self.this.is_open()
@property
def next(self):
ol = self.this
next_ol = ol.next()
if not next_ol.m_internal:
return
return Outline(next_ol)
@property
def page(self):
if 1:
return self.this.m_internal.page.page
return self.this.page().page
@property
def title(self):
return self.this.m_internal.title
@property
def uri(self):
ol = self.this
if not ol.m_internal:
return None
return ol.m_internal.uri
@property
def x(self):
return self.this.m_internal.x
@property
def y(self):
return self.this.m_internal.y
__slots__ = [ 'this']
def _make_PdfFilterOptions(
recurse=0,
instance_forms=0,
ascii=0,
no_update=0,
sanitize=0,
sopts=None,
):
'''
Returns a mupdf.PdfFilterOptions instance.
'''
filter_ = mupdf.PdfFilterOptions()
filter_.recurse = recurse
filter_.instance_forms = instance_forms
filter_.ascii = ascii
if mupdf_version_tuple >= (1, 22):
filter_.no_update = no_update
if sanitize:
if sopts:
assert isinstance(sopts, mupdf.PdfSanitizeFilterOptions)
else:
sopts = mupdf.PdfSanitizeFilterOptions()
class Factory(mupdf.PdfFilterFactory2):
def __init__(self):
super().__init__()
self.use_virtual_filter()
self.sopts = sopts
def filter(self, ctx, doc, chain, struct_parents, transform, options):
if 0:
log(f'sanitize filter.filter():')
log(f' {self=}')
log(f' {ctx=}')
log(f' {doc=}')
log(f' {chain=}')
log(f' {struct_parents=}')
log(f' {transform=}')
log(f' {options=}')
log(f' {self.sopts.internal()=}')
return mupdf.ll_pdf_new_sanitize_filter(
doc,
chain,
struct_parents,
transform,
options,
self.sopts.internal(),
)
factory = Factory()
filter_.add_factory(factory.internal())
filter_._factory = factory
else:
filter_.sanitize = sanitize
return filter_
class Page:
def __init__(self, page, document):
assert isinstance(page, (mupdf.FzPage, mupdf.PdfPage)), f'page is: {page}'
self.this = page
self.thisown = True
self.lastPoint = None
self.draw_cont = ''
self._annot_refs = dict()
self.parent = document
if page.m_internal:
if isinstance( page, mupdf.PdfPage):
self.number = page.m_internal.super.number
else:
self.number = page.m_internal.number
else:
self.number = None
def __repr__(self):
return self.__str__()
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 __str__(self):
parent = getattr(self, 'parent', None)
if isinstance(self.this.m_internal, mupdf.pdf_page):
number = self.this.m_internal.super.number
else:
number = self.this.m_internal.number
ret = f'page {number}'
if parent:
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
ret += f' of {x}'
return ret
def _add_caret_annot(self, point):
if g_use_extra:
annot = extra._add_caret_annot( self.this, JM_point_from_py(point))
elif g_use_extra:
if isinstance( self.this, mupdf.PdfPage):
page = self.this
else:
page = mupdf.pdf_page_from_fz_page( self.this)
annot = extra._add_caret_annot( page, JM_point_from_py(point))
else:
page = self._pdf_page()
annot = mupdf.pdf_create_annot(page, mupdf.PDF_ANNOT_CARET)
if point:
p = JM_point_from_py(point)
r = mupdf.pdf_annot_rect(annot)
r = mupdf.FzRect(p.x, p.y, p.x + r.x1 - r.x0, p.y + r.y1 - r.y0)
mupdf.pdf_set_annot_rect(annot, r)
mupdf.pdf_update_annot(annot)
JM_add_annot_id(annot, "A")
return annot
def _add_file_annot(self, point, buffer_, filename, ufilename=None, desc=None, icon=None):
page = self._pdf_page()
uf = ufilename if ufilename else filename
d = desc if desc else filename
p = JM_point_from_py(point)
ASSERT_PDF(page)
filebuf = JM_BufferFromBytes(buffer_)
if not filebuf.m_internal:
raise TypeError( MSG_BAD_BUFFER)
annot = mupdf.pdf_create_annot(page, mupdf.PDF_ANNOT_FILE_ATTACHMENT)
r = mupdf.pdf_annot_rect(annot)
r = mupdf.fz_make_rect(p.x, p.y, p.x + r.x1 - r.x0, p.y + r.y1 - r.y0)
mupdf.pdf_set_annot_rect(annot, r)
flags = mupdf.PDF_ANNOT_IS_PRINT
mupdf.pdf_set_annot_flags(annot, flags)
if icon:
mupdf.pdf_set_annot_icon_name(annot, icon)
val = JM_embed_file(page.doc(), filebuf, filename, uf, d, 1)
mupdf.pdf_dict_put(mupdf.pdf_annot_obj(annot), PDF_NAME('FS'), val)
mupdf.pdf_dict_put_text_string(mupdf.pdf_annot_obj(annot), PDF_NAME('Contents'), filename)
mupdf.pdf_update_annot(annot)
mupdf.pdf_set_annot_rect(annot, r)
mupdf.pdf_set_annot_flags(annot, flags)
JM_add_annot_id(annot, "A")
return Annot(annot)
def _add_freetext_annot(
self, rect,
text,
fontsize=11,
fontname=None,
text_color=None,
fill_color=None,
border_color=None,
align=0,
rotate=0,
):
page = self._pdf_page()
nfcol, fcol = JM_color_FromSequence(fill_color)
ntcol, tcol = JM_color_FromSequence(text_color)
r = JM_rect_from_py(rect)
if mupdf.fz_is_infinite_rect(r) or mupdf.fz_is_empty_rect(r):
raise ValueError( MSG_BAD_RECT)
annot = mupdf.pdf_create_annot( page, mupdf.PDF_ANNOT_FREE_TEXT)
annot_obj = mupdf.pdf_annot_obj( annot)
mupdf.pdf_set_annot_contents( annot, text)
mupdf.pdf_set_annot_rect( annot, r)
mupdf.pdf_dict_put_int( annot_obj, PDF_NAME('Rotate'), rotate)
mupdf.pdf_dict_put_int( annot_obj, PDF_NAME('Q'), align)
if nfcol > 0:
mupdf.pdf_set_annot_color( annot, fcol[:nfcol])
JM_make_annot_DA(annot, ntcol, tcol, fontname, fontsize)
mupdf.pdf_update_annot( annot)
JM_add_annot_id(annot, "A")
val = Annot(annot)
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 is not None:
ap = bwidth + fill_string + stroke_string + re + b"\n" + ope + b"\n" + ap
val._setAP(ap)
return val
def _add_ink_annot(self, list):
page = mupdf.pdf_page_from_fz_page(self.this)
ASSERT_PDF(page)
if not PySequence_Check(list):
raise ValueError( MSG_BAD_ARG_INK_ANNOT)
ctm = mupdf.FzMatrix()
mupdf.pdf_page_transform(page, mupdf.FzRect(0), ctm)
inv_ctm = mupdf.fz_invert_matrix(ctm)
annot = mupdf.pdf_create_annot(page, mupdf.PDF_ANNOT_INK)
annot_obj = mupdf.pdf_annot_obj(annot)
n0 = len(list)
inklist = mupdf.pdf_new_array(page.doc(), n0)
for j in range(n0):
sublist = list[j]
n1 = len(sublist)
stroke = mupdf.pdf_new_array(page.doc(), 2 * n1)
for i in range(n1):
p = sublist[i]
if not PySequence_Check(p) or PySequence_Size(p) != 2:
raise ValueError( MSG_BAD_ARG_INK_ANNOT)
point = mupdf.fz_transform_point(JM_point_from_py(p), inv_ctm)
mupdf.pdf_array_push_real(stroke, point.x)
mupdf.pdf_array_push_real(stroke, point.y)
mupdf.pdf_array_push(inklist, stroke)
mupdf.pdf_dict_put(annot_obj, PDF_NAME('InkList'), inklist)
mupdf.pdf_update_annot(annot)
JM_add_annot_id(annot, "A")
return Annot(annot)
def _add_line_annot(self, p1, p2):
page = self._pdf_page()
ASSERT_PDF(page)
annot = mupdf.pdf_create_annot(page, mupdf.PDF_ANNOT_LINE)
a = JM_point_from_py(p1)
b = JM_point_from_py(p2)
mupdf.pdf_set_annot_line(annot, a, b)
mupdf.pdf_update_annot(annot)
JM_add_annot_id(annot, "A")
assert annot.m_internal
return Annot(annot)
def _add_multiline(self, points, annot_type):
page = self._pdf_page()
if len(points) < 2:
raise ValueError( MSG_BAD_ARG_POINTS)
annot = mupdf.pdf_create_annot(page, annot_type)
for p in points:
if (PySequence_Size(p) != 2):
raise ValueError( MSG_BAD_ARG_POINTS)
point = JM_point_from_py(p)
mupdf.pdf_add_annot_vertex(annot, point)
mupdf.pdf_update_annot(annot)
JM_add_annot_id(annot, "A")
return Annot(annot)
def _add_redact_annot(self, quad, text=None, da_str=None, align=0, fill=None, text_color=None):
page = self._pdf_page()
fcol = [ 1, 1, 1, 0]
nfcol = 0
annot = mupdf.pdf_create_annot(page, mupdf.PDF_ANNOT_REDACT)
q = JM_quad_from_py(quad)
r = mupdf.fz_rect_from_quad(q)
mupdf.pdf_set_annot_rect(annot, r)
if fill:
nfcol, fcol = JM_color_FromSequence(fill)
arr = mupdf.pdf_new_array(page.doc(), nfcol)
for i in range(nfcol):
mupdf.pdf_array_push_real(arr, fcol[i])
mupdf.pdf_dict_put(mupdf.pdf_annot_obj(annot), PDF_NAME('IC'), arr)
if text:
mupdf.pdf_dict_puts(
mupdf.pdf_annot_obj(annot),
"OverlayText",
mupdf.pdf_new_text_string(text),
)
mupdf.pdf_dict_put_text_string(mupdf.pdf_annot_obj(annot), PDF_NAME('DA'), da_str)
mupdf.pdf_dict_put_int(mupdf.pdf_annot_obj(annot), PDF_NAME('Q'), align)
mupdf.pdf_update_annot(annot)
JM_add_annot_id(annot, "A")
annot = mupdf.ll_pdf_keep_annot(annot.m_internal)
annot = mupdf.PdfAnnot( annot)
return Annot(annot)
def _add_square_or_circle(self, rect, annot_type):
page = self._pdf_page()
r = JM_rect_from_py(rect)
if mupdf.fz_is_infinite_rect(r) or mupdf.fz_is_empty_rect(r):
raise ValueError( MSG_BAD_RECT)
annot = mupdf.pdf_create_annot(page, annot_type)
mupdf.pdf_set_annot_rect(annot, r)
mupdf.pdf_update_annot(annot)
JM_add_annot_id(annot, "A")
assert annot.m_internal
return Annot(annot)
def _add_stamp_annot(self, rect, stamp=0):
page = self._pdf_page()
stamp_id = [
PDF_NAME('Approved'),
PDF_NAME('AsIs'),
PDF_NAME('Confidential'),
PDF_NAME('Departmental'),
PDF_NAME('Experimental'),
PDF_NAME('Expired'),
PDF_NAME('Final'),
PDF_NAME('ForComment'),
PDF_NAME('ForPublicRelease'),
PDF_NAME('NotApproved'),
PDF_NAME('NotForPublicRelease'),
PDF_NAME('Sold'),
PDF_NAME('TopSecret'),
PDF_NAME('Draft'),
]
n = len(stamp_id)
name = stamp_id[0]
ASSERT_PDF(page)
r = JM_rect_from_py(rect)
if mupdf.fz_is_infinite_rect(r) or mupdf.fz_is_empty_rect(r):
raise ValueError( MSG_BAD_RECT)
if _INRANGE(stamp, 0, n-1):
name = stamp_id[stamp]
annot = mupdf.pdf_create_annot(page, mupdf.PDF_ANNOT_STAMP)
mupdf.pdf_set_annot_rect(annot, r)
try:
n = PDF_NAME('Name')
mupdf.pdf_dict_put(mupdf.pdf_annot_obj(annot), PDF_NAME('Name'), name)
except Exception:
if g_exceptions_verbose: exception_info()
raise
mupdf.pdf_set_annot_contents(
annot,
mupdf.pdf_dict_get_name(mupdf.pdf_annot_obj(annot), PDF_NAME('Name')),
)
mupdf.pdf_update_annot(annot)
JM_add_annot_id(annot, "A")
return Annot(annot)
def _add_text_annot(self, point, text, icon=None):
page = self._pdf_page()
p = JM_point_from_py( point)
ASSERT_PDF(page)
annot = mupdf.pdf_create_annot(page, mupdf.PDF_ANNOT_TEXT)
r = mupdf.pdf_annot_rect(annot)
r = mupdf.fz_make_rect(p.x, p.y, p.x + r.x1 - r.x0, p.y + r.y1 - r.y0)
mupdf.pdf_set_annot_rect(annot, r)
mupdf.pdf_set_annot_contents(annot, text)
if icon:
mupdf.pdf_set_annot_icon_name(annot, icon)
mupdf.pdf_update_annot(annot)
JM_add_annot_id(annot, "A")
return Annot(annot)
def _add_text_marker(self, quads, annot_type):
CheckParent(self)
if not self.parent.is_pdf:
raise ValueError("is no PDF")
val = 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 _addAnnot_FromString(self, linklist):
"""Add links from list of object sources."""
CheckParent(self)
if g_use_extra:
self.__class__._addAnnot_FromString = extra.Page_addAnnot_FromString
log('Page._addAnnot_FromString() deferring to extra.Page_addAnnot_FromString().')
return extra.Page_addAnnot_FromString( self.this, linklist)
page = mupdf.pdf_page_from_fz_page(self.this)
lcount = len(linklist)
if lcount < 1:
return
i = -1
ASSERT_PDF(page)
if not isinstance(linklist, tuple):
raise ValueError( "bad 'linklist' argument")
if not mupdf.pdf_dict_get( page.obj(), PDF_NAME('Annots')).m_internal:
mupdf.pdf_dict_put_array( page.obj(), PDF_NAME('Annots'), lcount)
annots = mupdf.pdf_dict_get( page.obj(), PDF_NAME('Annots'))
assert annots.m_internal, f'{lcount=} {annots.m_internal=}'
for i in range(lcount):
txtpy = linklist[i]
text = JM_StrAsChar(txtpy)
if not text:
PySys_WriteStderr("skipping bad link / annot item %i.\n", i)
continue
try:
annot = mupdf.pdf_add_object( page.doc(), JM_pdf_obj_from_str( page.doc(), text))
ind_obj = mupdf.pdf_new_indirect( page.doc(), mupdf.pdf_to_num( annot), 0)
mupdf.pdf_array_push( annots, ind_obj)
except Exception:
if g_exceptions_verbose: exception_info()
print("skipping bad link / annot item %i.\n" % i, file=sys.stderr)
def _addWidget(self, field_type, field_name):
page = self._pdf_page()
pdf = page.doc()
annot = JM_create_widget(pdf, page, field_type, field_name)
if not annot.m_internal:
raise RuntimeError( "cannot create widget")
JM_add_annot_id(annot, "W")
return Annot(annot)
def _apply_redactions(self, images):
page = self._pdf_page()
opts = mupdf.PdfRedactOptions()
opts.black_boxes = 0
opts.image_method = images
ASSERT_PDF(page)
success = mupdf.pdf_redact_page(page.doc(), page, opts)
return success
def _erase(self):
self._reset_annot_refs()
try:
self.parent._forget_page(self)
except Exception:
exception_info()
pass
self.parent = None
self.thisown = False
self.number = None
def _get_optional_content(self, oc: OptInt) -> OptStr:
if oc is 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_resource_properties(self):
'''
page list Resource/Properties
'''
page = self._pdf_page()
ASSERT_PDF(page)
rc = JM_get_resource_properties(page.obj())
return rc
def _get_textpage(self, clip=None, flags=0, matrix=None):
if g_use_extra:
ll_tpage = extra.page_get_textpage(self.this, clip, flags, matrix)
tpage = mupdf.FzStextPage(ll_tpage)
return tpage
page = self.this
options = mupdf.FzStextOptions(flags)
rect = JM_rect_from_py(clip)
rect = mupdf.fz_bound_page(page) if clip is None else JM_rect_from_py(clip)
ctm = JM_matrix_from_py(matrix)
tpage = mupdf.FzStextPage(rect)
dev = mupdf.fz_new_stext_device(tpage, options)
if g_no_device_caching:
mupdf.fz_enable_device_hints( dev, mupdf.FZ_NO_CACHE)
if isinstance(page, mupdf.FzPage):
pass
elif isinstance(page, mupdf.PdfPage):
page = page.super()
else:
assert 0, f'Unrecognised {type(page)=}'
mupdf.fz_run_page(page, dev, ctm, mupdf.FzCookie())
mupdf.fz_close_device(dev)
return tpage
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
):
maskbuf = mupdf.FzBuffer()
page = self._pdf_page()
pdf = page.doc()
w = width
h = height
img_xref = xref
rc_digest = 0
template = "\nq\n%g %g %g %g %g %g cm\n/%s Do\nQ\n"
do_process_pixmap = 1
do_process_stream = 1
do_have_imask = 1
do_have_image = 1
do_have_xref = 1
if xref > 0:
ref = mupdf.pdf_new_indirect(pdf, xref, 0)
w = mupdf.pdf_to_int( mupdf.pdf_dict_geta( ref, PDF_NAME('Width'), PDF_NAME('W')))
h = mupdf.pdf_to_int( mupdf.pdf_dict_geta( ref, PDF_NAME('Height'), PDF_NAME('H')))
if w + h == 0:
raise ValueError( MSG_IS_NO_IMAGE)
do_process_pixmap = 0
do_process_stream = 0
do_have_imask = 0
do_have_image = 0
else:
if stream:
imgbuf = JM_BufferFromBytes(stream)
do_process_pixmap = 0
else:
if filename:
imgbuf = mupdf.fz_read_file(filename)
do_process_pixmap = 0
if do_process_pixmap:
arg_pix = pixmap.this
w = arg_pix.w()
h = arg_pix.h()
digest = mupdf.fz_md5_pixmap2(arg_pix)
md5_py = digest
temp = digests.get(md5_py, None)
if temp is not None:
img_xref = temp
ref = mupdf.pdf_new_indirect(page.doc(), img_xref, 0)
do_process_stream = 0
do_have_imask = 0
do_have_image = 0
else:
if arg_pix.alpha() == 0:
image = mupdf.fz_new_image_from_pixmap(arg_pix, mupdf.FzImage())
else:
pm = mupdf.fz_convert_pixmap(
arg_pix,
mupdf.FzColorspace(0),
mupdf.FzColorspace(0),
mupdf.FzDefaultColorspaces(0),
mupdf.FzColorParams(),
1,
)
pm.alpha = 0
pm.colorspace = None
mask = mupdf.fz_new_image_from_pixmap(pm, mupdf.FzImage(0))
image = mupdf.fz_new_image_from_pixmap(arg_pix, mask)
do_process_stream = 0
do_have_imask = 0
if do_process_stream:
state = mupdf.FzMd5()
if mupdf_cppyy:
mupdf.fz_md5_update_buffer( state, imgbuf)
else:
mupdf.fz_md5_update(state, imgbuf.m_internal.data, imgbuf.m_internal.len)
if imask:
maskbuf = JM_BufferFromBytes(imask)
if mupdf_cppyy:
mupdf.fz_md5_update_buffer( state, maskbuf)
else:
mupdf.fz_md5_update(state, maskbuf.m_internal.data, maskbuf.m_internal.len)
digest = mupdf.fz_md5_final2(state)
md5_py = bytes(digest)
temp = digests.get(md5_py, None)
if temp is not None:
img_xref = temp
ref = mupdf.pdf_new_indirect(page.doc(), img_xref, 0)
w = mupdf.pdf_to_int( mupdf.pdf_dict_geta( ref, PDF_NAME('Width'), PDF_NAME('W')))
h = mupdf.pdf_to_int( mupdf.pdf_dict_geta( ref, PDF_NAME('Height'), PDF_NAME('H')))
do_have_imask = 0
do_have_image = 0
else:
image = mupdf.fz_new_image_from_buffer(imgbuf)
w = image.w()
h = image.h()
if not imask:
do_have_imask = 0
if do_have_imask:
cbuf1 = mupdf.FzCompressedBuffer( mupdf.ll_fz_compressed_image_buffer( image.m_internal))
if not cbuf1.m_internal:
raise ValueError( "uncompressed image cannot have mask")
bpc = image.bpc()
colorspace = image.colorspace()
xres, yres = mupdf.fz_image_resolution(image)
mask = mupdf.fz_new_image_from_buffer(maskbuf)
zimg = mupdf.fz_new_image_from_compressed_buffer(
w, h,
bpc, colorspace, xres, yres, 1, 0, None,
None, cbuf1, mask
)
image = zimg
if do_have_image:
ref = mupdf.pdf_add_image(pdf, image)
if oc:
JM_add_oc_object(pdf, ref, oc)
img_xref = mupdf.pdf_to_num(ref)
digests[md5_py] = img_xref
rc_digest = 1
if do_have_xref:
resources = mupdf.pdf_dict_get_inheritable(page.obj(), PDF_NAME('Resources'))
if not resources.m_internal:
resources = mupdf.pdf_dict_put_dict(page.obj(), PDF_NAME('Resources'), 2)
xobject = mupdf.pdf_dict_get(resources, PDF_NAME('XObject'))
if not xobject.m_internal:
xobject = mupdf.pdf_dict_put_dict(resources, PDF_NAME('XObject'), 2)
mat = calc_image_matrix(w, h, clip, rotate, keep_proportion)
mupdf.pdf_dict_puts(xobject, _imgname, ref)
nres = mupdf.fz_new_buffer(50)
s = template % (mat.a, mat.b, mat.c, mat.d, mat.e, mat.f, _imgname)
mupdf.fz_append_string(nres, s)
JM_insert_contents(pdf, page.obj(), nres, overlay)
if rc_digest:
return img_xref, digests
else:
return img_xref, None
def _insertFont(self, fontname, bfname, fontfile, fontbuffer, set_simple, idx, wmode, serif, encoding, ordering):
page = self._pdf_page()
ASSERT_PDF(page)
pdf = page.doc()
value = JM_insert_font(pdf, bfname, fontfile,fontbuffer, set_simple, idx, wmode, serif, encoding, ordering)
resources = mupdf.pdf_dict_get_inheritable( page.obj(), PDF_NAME('Resources'))
fonts = mupdf.pdf_dict_get(resources, PDF_NAME('Font'))
if not fonts.m_internal:
fonts = mupdf.pdf_new_dict(pdf, 5)
mupdf.pdf_dict_putl(page.obj(), fonts, PDF_NAME('Resources'), PDF_NAME('Font'))
_, xref = JM_INT_ITEM(value, 0)
if not xref:
raise RuntimeError( "cannot insert font")
font_obj = mupdf.pdf_new_indirect(pdf, xref, 0)
mupdf.pdf_dict_puts(fonts, fontname, font_obj)
return value
def _load_annot(self, name, xref):
page = self._pdf_page()
ASSERT_PDF(page)
if xref == 0:
annot = JM_get_annot_by_name(page, name)
else:
annot = JM_get_annot_by_xref(page, xref)
return Annot(annot) if annot else None
def _makePixmap(self, doc, ctm, cs, alpha=0, annots=1, clip=None):
pix = JM_pixmap_from_page(doc, self.this, ctm, cs, alpha, annots, clip)
return Pixmap(pix)
def _other_box(self, boxtype):
rect = mupdf.FzRect( mupdf.FzRect.Fixed_INFINITE)
page = mupdf.pdf_page_from_fz_page( self.this)
if page.m_internal:
obj = mupdf.pdf_dict_gets( page.obj(), boxtype)
if mupdf.pdf_is_array(obj):
rect = mupdf.pdf_to_rect(obj)
if mupdf.fz_is_infinite_rect( rect):
return
return JM_py_from_rect(rect)
def _pdf_page(self):
'''
Returns self.this as a mupdf.PdfPage using pdf_page_from_fz_page() if
required.
'''
if isinstance(self.this, mupdf.PdfPage):
return self.this
return mupdf.pdf_page_from_fz_page(self.this)
def _reset_annot_refs(self):
"""Invalidate / delete all annots of this page."""
self._annot_refs.clear()
def _set_opacity(self, gstate=None, CA=1, ca=1, blendmode=None):
if CA >= 1 and ca >= 1 and blendmode is None:
return
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)
if not gstate:
return
page = mupdf.pdf_page_from_fz_page(self.this)
ASSERT_PDF(page)
resources = mupdf.pdf_dict_get(page.obj(), PDF_NAME('Resources'))
if not resources.m_internal:
resources = mupdf.pdf_dict_put_dict(page.obj(), PDF_NAME('Resources'), 2)
extg = mupdf.pdf_dict_get(resources, PDF_NAME('ExtGState'))
if not extg.m_internal:
extg = mupdf.pdf_dict_put_dict(resources, PDF_NAME('ExtGState'), 2)
n = mupdf.pdf_dict_len(extg)
for i in range(n):
o1 = mupdf.pdf_dict_get_key(extg, i)
name = mupdf.pdf_to_name(o1)
if name == gstate:
return gstate
opa = mupdf.pdf_new_dict(page.doc(), 3)
mupdf.pdf_dict_put_real(opa, PDF_NAME('CA'), CA)
mupdf.pdf_dict_put_real(opa, PDF_NAME('ca'), ca)
mupdf.pdf_dict_puts(extg, gstate, opa)
return gstate
def _set_pagebox(self, boxtype, rect):
doc = self.parent
if doc is 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_resource_property(self, name, xref):
page = self._pdf_page()
ASSERT_PDF(page)
JM_set_resource_property(page.obj(), name, xref)
def _show_pdf_page(self, fz_srcpage, overlay=1, matrix=None, xref=0, oc=0, clip=None, graftmap=None, _imgname=None):
cropbox = JM_rect_from_py(clip)
mat = JM_matrix_from_py(matrix)
rc_xref = xref
tpage = mupdf.pdf_page_from_fz_page(self.this)
tpageref = tpage.obj()
pdfout = tpage.doc()
ENSURE_OPERATION(pdfout)
xobj1 = JM_xobject_from_page(pdfout, fz_srcpage, xref, graftmap.this)
if not rc_xref:
rc_xref = mupdf.pdf_to_num(xobj1)
subres1 = mupdf.pdf_new_dict(pdfout, 5)
mupdf.pdf_dict_puts(subres1, "fullpage", xobj1)
subres = mupdf.pdf_new_dict(pdfout, 5)
mupdf.pdf_dict_put(subres, PDF_NAME('XObject'), subres1)
res = mupdf.fz_new_buffer(20)
mupdf.fz_append_string(res, "/fullpage Do")
xobj2 = mupdf.pdf_new_xobject(pdfout, cropbox, mat, subres, res)
if oc > 0:
JM_add_oc_object(pdfout, mupdf.pdf_resolve_indirect(xobj2), oc)
resources = mupdf.pdf_dict_get_inheritable(tpageref, PDF_NAME('Resources'))
subres = mupdf.pdf_dict_get(resources, PDF_NAME('XObject'))
if not subres.m_internal:
subres = mupdf.pdf_dict_put_dict(resources, PDF_NAME('XObject'), 5)
mupdf.pdf_dict_puts(subres, _imgname, xobj2)
nres = mupdf.fz_new_buffer(50)
mupdf.fz_append_string(nres, " q /")
mupdf.fz_append_string(nres, _imgname)
mupdf.fz_append_string(nres, " Do Q ")
JM_insert_contents(pdfout, tpageref, nres, overlay)
return rc_xref
def add_caret_annot(self, point: point_like) -> 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 = Annot( annot)
annot_postprocess(self, annot)
assert hasattr( annot, 'parent')
return annot
def add_circle_annot(self, rect: rect_like) -> Annot:
"""Add a 'Circle' (ellipse, oval) annotation."""
old_rotation = annot_preprocess(self)
try:
annot = self._add_square_or_circle(rect, mupdf.PDF_ANNOT_CIRCLE)
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
) -> 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
) -> 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_highlight_annot(self, quads=None, start=None,
stop=None, clip=None) -> Annot:
"""Add a 'Highlight' annotation."""
if quads is None:
q = get_highlight_selection(self, start=start, stop=stop, clip=clip)
else:
q = CheckMarkerArg(quads)
ret = self._add_text_marker(q, mupdf.PDF_ANNOT_HIGHLIGHT)
return ret
def add_ink_annot(self, handwriting: list) -> 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_line_annot(self, p1: point_like, p2: point_like) -> 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_polygon_annot(self, points: list) -> Annot:
"""Add a 'Polygon' annotation."""
old_rotation = annot_preprocess(self)
try:
annot = self._add_multiline(points, mupdf.PDF_ANNOT_POLYGON)
finally:
if old_rotation != 0:
self.set_rotation(old_rotation)
annot_postprocess(self, annot)
return annot
def add_polyline_annot(self, points: list) -> Annot:
"""Add a 'PolyLine' annotation."""
old_rotation = annot_preprocess(self)
try:
annot = self._add_multiline(points, mupdf.PDF_ANNOT_POLY_LINE)
finally:
if old_rotation != 0:
self.set_rotation(old_rotation)
annot_postprocess(self, annot)
return annot
def add_rect_annot(self, rect: rect_like) -> Annot:
"""Add a 'Square' (rectangle) annotation."""
old_rotation = annot_preprocess(self)
try:
annot = self._add_square_or_circle(rect, mupdf.PDF_ANNOT_SQUARE)
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,
) -> 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)
if cross_out:
ap_tab = annot._getAP().splitlines()[:-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")
ap = b"\n".join(ap_tab)
annot._setAP(ap, 0)
return annot
def add_squiggly_annot(
self,
quads=None,
start=None,
stop=None,
clip=None,
) -> 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, mupdf.PDF_ANNOT_SQUIGGLY)
def add_stamp_annot(self, rect: rect_like, stamp: int =0) -> 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_strikeout_annot(self, quads=None, start=None, stop=None, clip=None) -> 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, mupdf.PDF_ANNOT_STRIKE_OUT)
def add_text_annot(self, point: point_like, text: str, icon: str ="Note") -> 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_underline_annot(self, quads=None, start=None, stop=None, clip=None) -> 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, mupdf.PDF_ANNOT_UNDERLINE)
def add_widget(self, widget: Widget) -> 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)
self._annot_refs[id(annot)] = annot
widget.parent = annot.parent
widget._annot = annot
widget.update()
return annot
def annot_names(self):
'''
page get list of annot names
'''
"""List of names of annotations, fields and links."""
CheckParent(self)
page = self._pdf_page()
if not page.m_internal:
return []
return JM_get_annot_id_list(page)
def annot_xrefs(self):
'''
List of xref numbers of annotations, fields and links.
'''
if g_use_extra:
return extra.JM_get_annot_xref_list2(self.this)
page = self._pdf_page()
if not page.m_internal:
return list()
return JM_get_annot_xref_list( page.obj())
def _unused_annot_xrefs(self):
"""List of xref numbers of annotations, fields and links."""
assert 0
CheckParent(self)
if 1 and g_use_extra:
assert 0
ret = extra.JM_get_annot_xref_list2( self.this)
return ret
page = self._pdf_page()
if not page.m_internal:
return []
ret = JM_get_annot_xref_list(page.obj())
return ret
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 = (mupdf.PDF_ANNOT_LINK, mupdf.PDF_ANNOT_POPUP, mupdf.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
@property
def artbox(self):
"""The ArtBox"""
rect = self._other_box("ArtBox")
if rect is 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 is None:
return self.cropbox
mb = self.mediabox
return Rect(rect[0], mb.y1 - rect[3], rect[2], mb.y1 - rect[1])
def bound(self):
"""Get page rectangle."""
CheckParent(self)
page = _as_fz_page(self.this)
val = mupdf.fz_bound_page(page)
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
def clean_contents(self, sanitize=1):
if not sanitize and not self.is_wrapped:
self.wrap_contents()
page = mupdf.pdf_page_from_fz_page( self.this)
if not page.m_internal:
return
filter_ = _make_PdfFilterOptions(recurse=1, sanitize=sanitize)
mupdf.pdf_filter_page_contents( page.doc(), page, filter_)
@property
def cropbox(self):
"""The CropBox."""
CheckParent(self)
page = self._pdf_page()
if not page.m_internal:
val = mupdf.fz_bound_page(self.this)
else:
val = JM_cropbox(page.obj())
val = Rect(val)
return val
@property
def cropbox_position(self):
return self.cropbox.tl
def delete_annot(self, annot):
"""Delete annot and return next one."""
CheckParent(self)
CheckParent(annot)
page = self._pdf_page()
while 1:
irt_annot = JM_find_annot_irt(annot.this)
if not irt_annot:
break
mupdf.pdf_delete_annot(page, irt_annot.this)
nextannot = mupdf.pdf_next_annot(annot.this)
mupdf.pdf_delete_annot(page, annot.this)
val = Annot(nextannot)
if val:
val.thisown = True
val.parent = weakref.proxy(self)
val.parent._annot_refs[id(val)] = val
annot._erase()
return val
def delete_link(self, linkdict):
"""Delete a Link."""
CheckParent(self)
if not isinstance( linkdict, dict):
return
def finished():
if linkdict["xref"] == 0: return
try:
linkid = linkdict["id"]
linkobj = self._annot_refs[linkid]
linkobj._erase()
except Exception:
pass
page = mupdf.pdf_page_from_fz_page( self.this)
if not page.m_internal:
return finished()
xref = linkdict[dictkey_xref]
if xref < 1:
return finished()
annots = mupdf.pdf_dict_get( page.obj(), PDF_NAME('Annots'))
if not annots.m_internal:
return finished()
len_ = mupdf.pdf_array_len( annots)
if len_ == 0:
return finished()
oxref = 0
for i in range( len_):
oxref = mupdf.pdf_to_num( mupdf.pdf_array_get( annots, i))
if xref == oxref:
break
if xref != oxref:
return finished()
mupdf.pdf_array_delete( annots, i)
mupdf.pdf_delete_object( page.doc(), xref)
mupdf.pdf_dict_put( page.obj(), PDF_NAME('Annots'), annots)
JM_refresh_links( page)
return finished()
@property
def derotation_matrix(self) -> Matrix:
"""Reflects page de-rotation."""
if g_use_extra:
return extra.Page_derotate_matrix( self.this)
pdfpage = self._pdf_page()
if not pdfpage.m_internal:
return JM_py_from_matrix(mupdf.FzRect(mupdf.FzRect.UNIT))
return JM_py_from_matrix(JM_derotate_page_matrix(pdfpage))
def extend_textpage(self, tpage, flags=0, matrix=None):
page = self.this
tp = tpage.this
assert isinstance( tp, mupdf.FzStextPage)
options = mupdf.FzStextOptions()
options.flags = flags
ctm = JM_matrix_from_py(matrix)
dev = mupdf.FzDevice(tp, options)
mupdf.fz_run_page( page, dev, ctm, mupdf.FzCookie())
mupdf.fz_close_device( dev)
@property
def first_annot(self):
"""First annotation."""
CheckParent(self)
page = self._pdf_page()
if not page:
return
annot = mupdf.pdf_first_annot(page)
if not annot.m_internal:
return
val = Annot(annot)
val.thisown = True
val.parent = weakref.proxy(self)
self._annot_refs[id(val)] = val
return val
@property
def first_link(self):
'''
First link on page
'''
return self.load_links()
@property
def first_widget(self):
"""First widget/field."""
CheckParent(self)
annot = 0
page = self._pdf_page()
if not page:
return
annot = mupdf.pdf_first_widget(page)
if not annot.m_internal:
return
val = Annot(annot)
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_bboxlog(self, layers=None):
CheckParent(self)
old_rotation = self.rotation
if old_rotation != 0:
self.set_rotation(0)
page = self.this
rc = []
inc_layers = True if layers else False
dev = JM_new_bbox_device( rc, inc_layers)
mupdf.fz_run_page( page, dev, mupdf.FzMatrix(), mupdf.FzCookie())
mupdf.fz_close_device( dev)
if old_rotation != 0:
self.set_rotation(old_rotation)
return rc
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)
page = self.this
if isinstance(page, mupdf.PdfPage):
page = mupdf.FzPage(page)
assert isinstance(page, mupdf.FzPage), f'{self.this=}'
clips = True if extended else False
prect = mupdf.fz_bound_page(page)
if g_use_extra:
rc = extra.get_cdrawings(page, extended, callback, method)
else:
rc = list()
if callable(callback) or method is not None:
dev = JM_new_lineart_device_Device(callback, clips, method)
else:
dev = JM_new_lineart_device_Device(rc, clips, method)
dev.ptm = mupdf.FzMatrix(1, 0, 0, -1, 0, prect.y1)
mupdf.fz_run_page(page, dev, mupdf.FzMatrix(), mupdf.FzCookie())
mupdf.fz_close_device(dev)
if old_rotation != 0:
self.set_rotation(old_rotation)
if callable(callback) or method is not None:
return
return rc
def get_contents(self):
"""Get xrefs of /Contents objects."""
CheckParent(self)
ret = []
page = mupdf.pdf_page_from_fz_page(self.this)
obj = page.obj()
contents = mupdf.pdf_dict_get(obj, mupdf.PDF_ENUM_NAME_Contents)
if mupdf.pdf_is_array(contents):
n = mupdf.pdf_array_len(contents)
for i in range(n):
icont = mupdf.pdf_array_get(contents, i)
xref = mupdf.pdf_to_num(icont)
ret.append(xref)
elif contents.m_internal:
xref = mupdf.pdf_to_num(contents)
ret.append( xref)
return ret
def get_displaylist(self, annots=1):
'''
Make a DisplayList from the page for Pixmap generation.
Include (default) or exclude annotations.
'''
CheckParent(self)
if annots:
dl = mupdf.fz_new_display_list_from_page(self.this)
else:
dl = mupdf.fz_new_display_list_from_page_contents(self.this)
return DisplayList(dl)
def get_drawings(self, extended: bool=False) -> list:
"""Retrieve vector graphics. The extended version includes clips.
Note:
For greater comfort, this method converts point-likes, rect-likes, quad-likes
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]), 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 __getitem__(self, item):
return self.paths.__getitem__(item)
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 __len__(self):
return self.paths.__len__()
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(
reversed(
[
p
for p in self.paths[:i]
if p.type == "clip" and p.level < lvl
]
)
)
if clips == []:
return []
nclips = [clips[0]]
for p in clips[1:]:
if p.level >= nclips[-1].level:
continue
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(
reversed(
[
p
for p in self.paths[:i]
if p.type == "group" and p.level < lvl
]
)
)
if groups == []:
return []
ngroups = [groups[0]]
for p in groups[1:]:
if p.level >= ngroups[-1].level:
continue
ngroups.append(p)
return ngroups
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]), 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_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_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 is True:
try:
return self.get_image_rects(item, transform=transform)[0]
except Exception:
exception_info()
return inf_rect
pdf_page = self._pdf_page()
val = JM_image_reporter(pdf_page)
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 get_images(self, full=False):
"""List of images defined in the page object."""
CheckParent(self)
return self.parent.get_page_images(self.number, full=full)
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 get_svg_image(self, matrix=None, text_as_path=1):
"""Make SVG image from page."""
CheckParent(self)
mediabox = mupdf.fz_bound_page(self.this)
ctm = JM_matrix_from_py(matrix)
tbounds = mediabox
text_option = mupdf.FZ_SVG_TEXT_AS_PATH if text_as_path == 1 else mupdf.FZ_SVG_TEXT_AS_TEXT
tbounds = mupdf.fz_transform_rect(tbounds, ctm)
res = mupdf.fz_new_buffer(1024)
out = mupdf.FzOutput(res)
dev = mupdf.fz_new_svg_device(
out,
tbounds.x1-tbounds.x0,
tbounds.y1-tbounds.y0,
text_option,
1,
)
mupdf.fz_run_page(self.this, dev, ctm, mupdf.FzCookie())
mupdf.fz_close_device(dev)
text = JM_EscapeStrFromBuffer(res)
return text
def get_textbox(
page: Page,
rect: rect_like,
textpage=None,
) -> str:
tp = textpage
if tp is None:
tp = page.get_textpage()
elif getattr(tp, "parent") != page:
raise ValueError("not a textpage of this page")
rc = tp.extractTextbox(rect)
if textpage is None:
del tp
return rc
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)
textpage = TextPage(textpage)
return textpage
def get_texttrace(self):
CheckParent(self)
old_rotation = self.rotation
if old_rotation != 0:
self.set_rotation(0)
page = self.this
rc = []
if g_use_extra:
dev = extra.JM_new_texttrace_device(rc)
else:
dev = JM_new_texttrace_device(rc)
prect = mupdf.fz_bound_page(page)
dev.ptm = mupdf.FzMatrix(1, 0, 0, -1, 0, prect.y1)
mupdf.fz_run_page(page, dev, mupdf.FzMatrix(), mupdf.FzCookie())
mupdf.fz_close_device(dev)
if old_rotation != 0:
self.set_rotation(old_rotation)
return rc
def get_xobjects(self):
"""List of xobjects defined in the page object."""
CheckParent(self)
return self.parent.get_page_xobjects(self.number)
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:
xref = font[0]
if CheckFontInfo(doc, xref):
return xref
doc.get_char_widths(xref)
return xref
bfname = Base14_fontdict.get(fontname.lower(), None)
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 Exception:
pass
if CJK_number < 0:
try:
CJK_number = CJK_list_s.index(fontname)
serif = 1
except Exception:
pass
if fontname.lower() in fitz_fontdescriptors.keys():
import pymupdf_fonts
fontbuffer = pymupdf_fonts.myfont(fontname)
del pymupdf_fonts
if fontfile is not 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:
return val
xref = val[0]
fontdict = val[1]
if CheckFontInfo(doc, xref):
return xref
doc.get_char_widths(xref, fontdict=fontdict)
return xref
@property
def is_wrapped(self):
"""Check if /Contents is wrapped with string pair "q" / "Q"."""
if getattr(self, "was_wrapped", False):
return True
cont = self.read_contents().split()
if cont == []:
self.was_wrapped = True
return True
if cont[0] != b"q" or cont[-1] != b"Q":
return False
self.was_wrapped = True
return True
@property
def language(self):
"""Page language."""
pdfpage = mupdf.pdf_page_from_fz_page(self.this)
if not pdfpage.m_internal:
return
lang = mupdf.pdf_dict_get_inheritable(pdfpage.obj(), PDF_NAME('Lang'))
if not lang.m_internal:
return
return mupdf.pdf_to_str_buf(lang)
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.getLinks()
for link in all_links:
if kinds is None or link["kind"] in kinds:
yield (link)
def load_annot(self, ident: typing.Union[str, int]) -> 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 a 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
def load_links(self):
"""Get first Link."""
CheckParent(self)
val = mupdf.fz_load_links( self.this)
if not val.m_internal:
return
val = Link( val)
val.thisown = True
val.parent = weakref.proxy(self)
self._annot_refs[id(val)] = val
val.xref = 0
val.id = ""
if self.parent.is_pdf:
xrefs = self.annot_xrefs()
xrefs = [x for x in xrefs if x[1] == mupdf.PDF_ANNOT_LINK]
if xrefs:
link_id = xrefs[0]
val.xref = link_id[0]
val.id = link_id[2]
else:
val.xref = 0
val.id = ""
return val
def load_widget( self, xref):
"""Load a widget by its xref."""
CheckParent(self)
page = mupdf.pdf_page_from_fz_page( self.this)
ASSERT_PDF(page)
annot = JM_get_widget_by_xref( page, xref)
val = annot
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
@property
def mediabox(self):
"""The MediaBox."""
CheckParent(self)
page = self._pdf_page()
if not page.m_internal:
rect = mupdf.fz_bound_page( self.this)
else:
rect = JM_mediabox( page.obj())
return Rect(rect)
@property
def mediabox_size(self):
return Point(self.mediabox.x1, self.mediabox.y1)
def read_contents(self):
"""All /Contents streams concatenated to one bytes object."""
return TOOLS._get_all_contents(self)
def refresh(self):
"""Refresh page after link/annot/widget updates."""
CheckParent(self)
doc = self.parent
page = doc.reload_page(self)
self.this = page
@property
def rotation(self):
"""Page rotation."""
CheckParent(self)
page = self.this if isinstance(self.this, mupdf.PdfPage) else mupdf.pdf_page_from_fz_page(self.this)
if not page:
return 0
return JM_page_rotation(page)
@property
def rotation_matrix(self) -> Matrix:
"""Reflects page rotation."""
return Matrix(TOOLS._rotate_matrix(self))
def run(self, dw, m):
"""Run page through a device.
dw: DeviceWrapper
"""
CheckParent(self)
mupdf.fz_run_page(self.this, dw.device, JM_matrix_from_py(m), mupdf.FzCookie())
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_contents(self, xref):
"""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 xref not 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)
def set_cropbox(self, rect):
"""Set the CropBox. Will also change Page.rect."""
return self._set_pagebox("CropBox", rect)
def set_language(self, language=None):
"""Set PDF page default language."""
CheckParent(self)
pdfpage = mupdf.pdf_page_from_fz_page(self.this)
ASSERT_PDF(pdfpage)
if not language:
mupdf.pdf_dict_del(pdfpage.obj(), PDF_NAME('Lang'))
else:
lang = mupdf.fz_text_language_from_string(language)
assert hasattr(mupdf, 'fz_string_from_text_language2')
mupdf.pdf_dict_put_text_string(
pdfpage.obj,
PDF_NAME('Lang'),
mupdf.fz_string_from_text_language2(lang)
)
def set_mediabox(self, rect):
"""Set the MediaBox."""
CheckParent(self)
page = self._pdf_page()
ASSERT_PDF(page)
mediabox = JM_rect_from_py(rect)
if (mupdf.fz_is_empty_rect(mediabox)
or mupdf.fz_is_infinite_rect(mediabox)
):
raise ValueError( MSG_BAD_RECT)
mupdf.pdf_dict_put_rect( page.obj(), PDF_NAME('MediaBox'), mediabox)
mupdf.pdf_dict_del( page.obj(), PDF_NAME('CropBox'))
mupdf.pdf_dict_del( page.obj(), PDF_NAME('ArtBox'))
mupdf.pdf_dict_del( page.obj(), PDF_NAME('BleedBox'))
mupdf.pdf_dict_del( page.obj(), PDF_NAME('TrimBox'))
def set_rotation(self, rotation):
"""Set page rotation."""
CheckParent(self)
page = mupdf.pdf_page_from_fz_page( self.this)
ASSERT_PDF(page)
rot = JM_norm_rotation(rotation)
mupdf.pdf_dict_put_int( page.obj(), PDF_NAME('Rotate'), rot)
def set_trimbox(self, rect):
"""Set the TrimBox."""
return self._set_pagebox("TrimBox", rect)
@property
def transformation_matrix(self):
"""Page transformation matrix."""
CheckParent(self)
ctm = mupdf.FzMatrix()
page = self._pdf_page()
if not page.m_internal:
return JM_py_from_matrix(ctm)
mediabox = mupdf.FzRect(mupdf.FzRect.Fixed_UNIT)
mupdf.pdf_page_transform(page, mediabox, ctm)
val = JM_py_from_matrix(ctm)
if self.rotation % 360 == 0:
val = Matrix(val)
else:
val = Matrix(1, 0, 0, -1, 0, self.cropbox.height)
return val
@property
def trimbox(self):
"""The TrimBox"""
rect = self._other_box("TrimBox")
if rect is None:
return self.cropbox
mb = self.mediabox
return Rect(rect[0], mb.y1 - rect[3], rect[2], mb.y1 - rect[1])
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] == mupdf.PDF_ANNOT_WIDGET]
for xref in widget_xrefs:
widget = self.load_widget(xref)
if types is None or widget.field_type in types:
yield (widget)
def wrap_contents(self):
if self.is_wrapped:
return
TOOLS._insert_contents(self, b"q\n", False)
TOOLS._insert_contents(self, b"\nQ", True)
self.was_wrapped = True
@property
def xref(self):
"""PDF xref number of page."""
CheckParent(self)
return self.parent.page_xref(self.number)
rect = property(bound, doc="page rectangle")
if g_use_extra:
Page.annot_xrefs = extra.JM_get_annot_xref_list2
class 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=1) - copy and add or drop alpha channel.
Pixmap(filename) - from an image in a file.
Pixmap(image) - from an image in memory (bytes).
Pixmap(colorspace, width, height, samples, alpha) - from samples data.
Pixmap(PDFdoc, xref) - from an image at xref in a PDF document.
"""
if 0:
pass
elif args_match(args,
(Colorspace, mupdf.FzColorspace),
(mupdf.FzRect, mupdf.FzIrect, IRect, Rect, tuple)
):
cs, rect = args
alpha = 0
pm = mupdf.fz_new_pixmap_with_bbox(cs, JM_irect_from_py(rect), mupdf.FzSeparations(0), alpha)
self.this = pm
elif args_match(args,
(Colorspace, mupdf.FzColorspace),
(mupdf.FzRect, mupdf.FzIrect, IRect, Rect, tuple),
(int, bool)
):
cs, rect, alpha = args
pm = mupdf.fz_new_pixmap_with_bbox(cs, JM_irect_from_py(rect), mupdf.FzSeparations(0), alpha)
self.this = pm
elif args_match(args, mupdf.FzColorspace, mupdf.FzPixmap):
cs, spix = args
if not mupdf.fz_pixmap_colorspace(spix).m_internal:
raise ValueError( "source colorspace must not be None")
if cs.m_internal:
self.this = mupdf.fz_convert_pixmap(
spix,
cs,
mupdf.FzColorspace(0),
mupdf.FzDefaultColorspaces(0),
mupdf.FzColorParams(),
1
)
else:
self.this = mupdf.fz_new_pixmap_from_alpha_channel( spix)
if not self.this.m_internal:
raise RuntimeError( MSG_PIX_NOALPHA)
elif args_match(args, mupdf.FzPixmap, mupdf.FzPixmap):
spix, mpix = args
spm = spix
mpm = mpix
if not spix.m_internal:
dst = mupdf.fz_new_pixmap_from_alpha_channel( mpm)
if not dst.m_internal:
raise RuntimeError( MSG_PIX_NOALPHA)
else:
dst = mupdf.fz_new_pixmap_from_color_and_mask( spm, mpm)
return dst
elif args_match(args, (Pixmap, mupdf.FzPixmap), (float, int), (float, int), None):
assert 0, f'Cannot handle {args=} because fz_scale_pixmap() and fz_scale_pixmap_cached() are not declared in MuPDF headers'
spix, w, h, clip = args
src_pix = spix.this if isinstance(spix, Pixmap) else spix
bbox = JM_irect_from_py(clip)
if not mupdf.fz_is_infinite_irect(bbox):
pm = mupdf.fz_scale_pixmap_cached(src_pix, src_pix.x, src_pix.y, w, h, bbox)
else:
pm = mupdf.fz_scale_pixmap(src_pix, src_pix.x, src_pix.y, w, h, None)
self.this = pm
elif args_match(args, str, mupdf.FzPixmap) and args[0] == 'raw':
_, pm = args
self.this = pm
elif args_match(args, (Pixmap, mupdf.FzPixmap), (int, None)):
spix = args[0]
alpha = args[1] if len(args) == 2 else 1
src_pix = spix.this if isinstance(spix, Pixmap) else spix
if not _INRANGE(alpha, 0, 1):
raise ValueError( "bad alpha value")
cs = mupdf.fz_pixmap_colorspace(src_pix)
if not cs.m_internal and not alpha:
raise ValueError( "cannot drop alpha for 'NULL' colorspace")
seps = mupdf.FzSeparations()
n = mupdf.fz_pixmap_colorants(src_pix)
w = mupdf.fz_pixmap_width(src_pix)
h = mupdf.fz_pixmap_height(src_pix)
pm = mupdf.fz_new_pixmap(cs, w, h, seps, alpha)
pm.m_internal.x = src_pix.m_internal.x
pm.m_internal.y = src_pix.m_internal.y
pm.m_internal.xres = src_pix.m_internal.xres
pm.m_internal.yres = src_pix.m_internal.yres
if 1:
mupdf.ll_fz_pixmap_copy( pm.m_internal, src_pix.m_internal, n)
elif 1:
src_view = mupdf.fz_pixmap_samples_memoryview( src_pix)
pm_view = mupdf.fz_pixmap_samples_memoryview( pm)
if src_pix.alpha() == pm.alpha():
size = w * h * (n + alpha)
pm_view[ 0 : size] = src_view[ 0 : size]
else:
tptr = 0
sptr = 0
pm_stride = pm.stride()
pm_n = pm.n()
pm_alpha = pm.alpha()
src_stride = src_pix.stride()
src_n = src_pix.n()
for y in range( h):
for x in range( w):
pm_i = pm_stride * y + pm_n * x
src_i = src_stride * y + src_n * x
pm_view[ pm_i : pm_i + n] = src_view[ src_i : src_i + n]
if pm_alpha:
pm_view[ pm_i + n] = 255
else:
if src_pix.alpha() == pm.alpha():
for i in range(w * h * (n + alpha)):
mupdf.fz_samples_set(pm, i, mupdf.fz_samples_get(src_pix, i))
else:
tptr = 0
sptr = 0
src_pix_alpha = src_pix.alpha()
for i in range(w * h):
for j in range(n):
mupdf.fz_samples_set(pm, tptr + j, mupdf.fz_samples_get(src_pix, sptr + j))
tptr += n
if pm.alpha():
mupdf.fz_samples_set(pm, tptr, 255)
tptr += 1
sptr += n + src_pix_alpha
self.this = pm
elif args_match(args, (mupdf.FzColorspace, Colorspace), int, int, None, (int, bool)):
cs, w, h, samples, alpha = args
if isinstance(cs, Colorspace):
cs = cs.this
assert isinstance(cs, mupdf.FzColorspace)
n = mupdf.fz_colorspace_n(cs)
stride = (n + alpha) * w
seps = mupdf.FzSeparations()
pm = mupdf.fz_new_pixmap(cs, w, h, seps, alpha)
if isinstance( samples, (bytes, bytearray)):
log('using mupdf.python_buffer_data()')
samples2 = mupdf.python_buffer_data(samples)
size = len(samples)
else:
res = JM_BufferFromBytes(samples)
if not res.m_internal:
raise ValueError( "bad samples data")
size, c = mupdf.fz_buffer_storage(res)
samples2 = mupdf.python_buffer_data(samples)
if stride * h != size:
raise ValueError( f"bad samples length {w=} {h=} {alpha=} {n=} {stride=} {size=}")
mupdf.ll_fz_pixmap_copy_raw( pm.m_internal, samples2)
self.this = pm
elif args_match(args, None):
imagedata, = args
name = 'name'
if hasattr(imagedata, "resolve"):
fname = imagedata.__str__()
if fname:
img = mupdf.fz_new_image_from_file(fname)
elif hasattr(imagedata, name):
fname = imagedata.name
if fname:
img = mupdf.fz_new_image_from_file(fname)
elif isinstance(imagedata, str):
img = mupdf.fz_new_image_from_file(imagedata)
else:
res = JM_BufferFromBytes(imagedata)
if not res.m_internal or not res.m_internal.len:
raise ValueError( "bad image data")
img = mupdf.fz_new_image_from_buffer(res)
pm, w, h = mupdf.fz_get_pixmap_from_image(
img,
mupdf.FzIrect(FZ_MIN_INF_RECT, FZ_MIN_INF_RECT, FZ_MAX_INF_RECT, FZ_MAX_INF_RECT),
mupdf.FzMatrix( img.w(), 0, 0, img.h(), 0, 0),
)
xres, yres = mupdf.fz_image_resolution(img)
pm.xres = xres
pm.yres = yres
self.this = pm
elif args_match(args, (Document, mupdf.FzDocument), int):
doc, xref = args
pdf = _as_pdf_document(doc)
ASSERT_PDF(pdf)
xreflen = mupdf.pdf_xref_len(pdf)
if not _INRANGE(xref, 1, xreflen-1):
raise ValueError( MSG_BAD_XREF)
ref = mupdf.pdf_new_indirect(pdf, xref, 0)
type_ = mupdf.pdf_dict_get(ref, PDF_NAME('Subtype'))
if (not mupdf.pdf_name_eq(type_, PDF_NAME('Image'))
and not mupdf.pdf_name_eq(type_, PDF_NAME('Alpha'))
and not mupdf.pdf_name_eq(type_, PDF_NAME('Luminosity'))
):
raise ValueError( MSG_IS_NO_IMAGE)
img = mupdf.pdf_load_image(pdf, ref)
pix, w, h = mupdf.fz_get_pixmap_from_image(
img,
mupdf.FzIrect(FZ_MIN_INF_RECT, FZ_MIN_INF_RECT, FZ_MAX_INF_RECT, FZ_MAX_INF_RECT),
mupdf.FzMatrix(img.w(), 0, 0, img.h(), 0, 0),
)
self.this = pix
else:
text = 'Unrecognised args for constructing Pixmap:\n'
for arg in args:
text += f' {type(arg)}: {arg}\n'
raise Exception( text)
def __len__(self):
return self.size
def __repr__(self):
if not type(self) is Pixmap: return
if self.colorspace:
return "Pixmap(%s, %s, %s)" % (self.colorspace.this.m_internal.name, self.irect, self.alpha)
else:
return "Pixmap(%s, %s, %s)" % ('None', self.irect, self.alpha)
def _tobytes(self, format_, jpg_quality):
'''
Pixmap._tobytes
'''
pm = self.this
size = mupdf.fz_pixmap_stride(pm) * pm.h()
res = mupdf.fz_new_buffer(size)
out = mupdf.FzOutput(res)
if format_ == 1: mupdf.fz_write_pixmap_as_png(out, pm)
elif format_ == 2: mupdf.fz_write_pixmap_as_pnm(out, pm)
elif format_ == 3: mupdf.fz_write_pixmap_as_pam(out, pm)
elif format_ == 5: mupdf.fz_write_pixmap_as_psd(out, pm)
elif format_ == 6: mupdf.fz_write_pixmap_as_ps(out, pm)
elif format_ == 7: mupdf.fz_write_pixmap_as_jpeg(out, pm, jpg_quality)
else: mupdf.fz_write_pixmap_as_png(out, pm)
barray = JM_BinFromBuffer(res)
return barray
def _writeIMG(self, filename, format_, jpg_quality):
pm = self.this
if format_ == 1: mupdf.fz_save_pixmap_as_png(pm, filename)
elif format_ == 2: mupdf.fz_save_pixmap_as_pnm(pm, filename)
elif format_ == 3: mupdf.fz_save_pixmap_as_pam(pm, filename)
elif format_ == 5: mupdf.fz_save_pixmap_as_psd(pm, filename)
elif format_ == 6: mupdf.fz_save_pixmap_as_ps(pm, filename)
elif format_ == 7: mupdf.fz_save_pixmap_as_jpeg(pm, filename, jpg_quality)
else: mupdf.fz_save_pixmap_as_png(pm, filename)
@property
def alpha(self):
"""Indicates presence of alpha channel."""
return mupdf.fz_pixmap_alpha(self.this)
def clear_with(self, value=None, bbox=None):
"""Fill all color components with same value."""
if value is None:
mupdf.fz_clear_pixmap(self.this)
elif bbox is None:
mupdf.fz_clear_pixmap_with_value(self.this, value)
else:
JM_clear_pixmap_rect_with_value(self.this, value, JM_irect_from_py(bbox))
def color_count(self, colors=0, clip=None):
'''
Return count of each color.
'''
pm = self.this
rc = JM_color_count( pm, clip)
if not rc:
raise RuntimeError( MSG_COLOR_COUNT_FAILED)
if not colors:
return len( rc)
return rc
def color_topusage(self, clip=None):
"""Return most frequent color and its usage ratio."""
allpixels = 0
cnt = 0
if clip is not 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 colorspace(self):
"""Pixmap Colorspace."""
return Colorspace(mupdf.fz_pixmap_colorspace(self.this))
def copy(self, src, bbox):
"""Copy bbox from another Pixmap."""
pm = self.this
src_pix = src.this
if not mupdf.fz_pixmap_colorspace(src_pix):
raise ValueError( "cannot copy pixmap with NULL colorspace")
if pm.alpha() != src_pix.alpha():
raise ValueError( "source and target alpha must be equal")
mupdf.fz_copy_pixmap_rect(pm, src_pix, JM_irect_from_py(bbox), mupdf.FzDefaultColorspaces(None))
@property
def digest(self):
"""MD5 digest of pixmap (bytes)."""
ret = mupdf.fz_md5_pixmap2(self.this)
return bytes(ret)
def gamma_with(self, gamma):
"""Apply correction with some float.
gamma=1 is a no-op."""
if not mupdf.fz_pixmap_colorspace( self.this):
JM_Warning("colorspace invalid for function")
return
mupdf.fz_gamma_pixmap( self.this, gamma)
@property
def h(self):
"""The height."""
return mupdf.fz_pixmap_height(self.this)
def invert_irect(self, bbox=None):
"""Invert the colors inside a bbox."""
pm = self.this
if not mupdf.fz_pixmap_colorspace(pm):
JM_Warning("ignored for stencil pixmap")
return False
r = JM_irect_from_py(bbox)
if mupdf.fz_is_infinite_irect(r):
r = mupdf.fz_pixmap_bbox( pm)
return bool(JM_invert_pixmap_rect( pm, r))
@property
def irect(self):
"""Pixmap bbox - an IRect object."""
val = mupdf.fz_pixmap_bbox(self.this)
return JM_py_from_irect( val)
@property
def is_monochrome(self):
"""Check if pixmap is monochrome."""
return mupdf.fz_is_pixmap_monochrome( self.this)
@property
def is_unicolor(self):
'''
Check if pixmap has only one color.
'''
pm = self.this
n = pm.n()
count = pm.w() * pm.h() * n
def _pixmap_read_samples(pm, offset, n):
ret = list()
for i in range(n):
ret.append(mupdf.fz_samples_get(pm, offset+i))
return ret
sample0 = _pixmap_read_samples( pm, 0, n)
for offset in range( n, count, n):
sample = _pixmap_read_samples( pm, offset, n)
if sample != sample0:
return False
return True
@property
def n(self):
"""The size of one pixel."""
if g_use_extra:
def n2(self):
return extra.pixmap_n(self.this)
self.__class__.n = property(n2)
return self.n
return mupdf.fz_pixmap_components(self.this)
def pdfocr_save(self, filename, compress=1, language=None, tessdata=None):
'''
Save pixmap as an OCR-ed PDF page.
'''
if not TESSDATA_PREFIX and not tessdata:
raise RuntimeError('No OCR support: TESSDATA_PREFIX not set')
opts = mupdf.FzPdfocrOptions()
opts.compress = compress
if language:
opts.language_set2( language)
if tessdata:
opts.datadir_set2( tessdata)
pix = self.this
if isinstance(filename, str):
mupdf.fz_save_pixmap_as_pdfocr( pix, filename, 0, opts)
else:
out = JM_new_output_fileptr( filename)
mupdf.fz_write_pixmap_as_pdfocr( out, pix, opts)
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 TESSDATA_PREFIX and not tessdata:
raise RuntimeError('No OCR support: TESSDATA_PREFIX not set')
from io import BytesIO
bio = BytesIO()
self.pdfocr_save(bio, compress=compress, language=language, tessdata=tessdata)
return bio.getvalue()
def pil_save(self, *args, **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.
"""
try:
from PIL import Image
except ImportError:
print("PIL/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"
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, **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.
"""
from io import BytesIO
bytes_out = BytesIO()
self.pil_save(bytes_out, *args, **kwargs)
return bytes_out.getvalue()
def pixel(self, x, y):
"""Get color tuple of pixel (x, y).
Last item is the alpha if Pixmap.alpha is true."""
if g_use_extra:
return extra.pixmap_pixel(self.this.m_internal, x, y)
if (0
or x < 0
or x >= self.this.m_internal.w
or y < 0
or y >= self.this.m_internal.h
):
RAISEPY(MSG_PIXEL_OUTSIDE, PyExc_ValueError)
n = self.this.m_internal.n
stride = self.this.m_internal.stride
i = stride * y + n * x
ret = tuple( self.samples_mv[ i: i+n])
return ret
@property
def samples(self)->bytes:
mv = self.samples_mv
return bytes( mv)
@property
def samples_mv(self):
'''
Pixmap samples memoryview.
'''
return mupdf.fz_pixmap_samples_memoryview(self.this)
@property
def samples_ptr(self):
return mupdf.fz_pixmap_samples_int(self.this)
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.
"""
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 is 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 set_alpha(self, alphavalues=None, premultiply=1, opaque=None, matte=None):
"""Set alpha channel to values contained in a byte array.
If omitted, set alphas to 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.
"""
pix = self.this
alpha = 0
m = 0
if pix.alpha() == 0:
raise ValueError( MSG_PIX_NOALPHA)
n = mupdf.fz_pixmap_colorants(pix)
w = mupdf.fz_pixmap_width(pix)
h = mupdf.fz_pixmap_height(pix)
balen = w * h * (n+1)
colors = [0, 0, 0, 0]
bgcolor = [0, 0, 0, 0]
zero_out = 0
bground = 0
if opaque and isinstance(opaque, (list, tuple)) and len(opaque) == n:
for i in range(n):
colors[i] = opaque[i]
zero_out = 1
if matte and isinstance( matte, (tuple, list)) and len(matte) == n:
for i in range(n):
bgcolor[i] = matte[i]
bground = 1
data = bytes()
data_len = 0
if alphavalues:
if isinstance(alphavalues, (bytes, bytearray)):
data = alphavalues
data_len = len(alphavalues)
else:
assert 0, f'unexpected type for alphavalues: {type(alphavalues)}'
if data_len < w * h:
raise ValueError( "bad alpha values")
if 1:
mupdf.Pixmap_set_alpha_helper(
balen,
n,
data_len,
zero_out,
mupdf.python_buffer_data( data),
pix.m_internal,
premultiply,
bground,
colors,
bgcolor,
)
else:
i = k = j = 0
data_fix = 255
while i < balen:
alpha = data[k]
if zero_out:
for j in range(i, i+n):
if mupdf.fz_samples_get(pix, j) != colors[j - i]:
data_fix = 255
break
else:
data_fix = 0
if data_len:
def fz_mul255( a, b):
x = a * b + 128
x += x // 256
return x // 256
if data_fix == 0:
mupdf.fz_samples_set(pix, i+n, 0)
else:
mupdf.fz_samples_set(pix, i+n, alpha)
if premultiply and not bground:
for j in range(i, i+n):
mupdf.fz_samples_set(pix, j, fz_mul255( mupdf.fz_samples_get(pix, j), alpha))
elif bground:
for j in range( i, i+n):
m = bgcolor[j - i]
mupdf.fz_samples_set(pix, j, fz_mul255( mupdf.fz_samples_get(pix, j) - m, alpha))
else:
mupdf.fz_samples_set(pix, i+n, data_fix)
i += n+1
k += 1
def tobytes(self, output="png", jpg_quality=95):
'''
Convert to binary image stream of desired type.
'''
valid_formats = {
"png": 1,
"pnm": 2,
"pgm": 2,
"ppm": 2,
"pbm": 2,
"pam": 3,
"tga": 4,
"tpic": 4,
"psd": 5,
"ps": 6,
'jpg': 7,
'jpeg': 7,
}
idx = valid_formats.get(output.lower(), None)
if idx is None:
raise ValueError(f"Image format {output} not in {tuple(valid_formats.keys())}")
if self.alpha and idx in (2, 6, 7):
raise ValueError("'{output}' cannot have alpha")
if self.colorspace and self.colorspace.n > 3 and idx in (1, 2, 4):
raise ValueError("unsupported colorspace for '{output}'")
if idx == 7:
self.set_dpi(self.xres, self.yres)
barray = self._tobytes(idx, jpg_quality)
return barray
def set_dpi(self, xres, yres):
"""Set resolution in both dimensions."""
pm = self.this
pm.m_internal.xres = xres
pm.m_internal.yres = yres
def set_origin(self, x, y):
"""Set top-left coordinates."""
pm = self.this
pm.m_internal.x = x
pm.m_internal.y = y
def set_pixel(self, x, y, color):
"""Set color of pixel (x, y)."""
pm = self.this
if not _INRANGE(x, 0, pm.w() - 1) or not _INRANGE(y, 0, pm.h() - 1):
raise ValueError( MSG_PIXEL_OUTSIDE)
n = pm.n()
c = list()
for j in range(n):
i = color[j]
if not _INRANGE(i, 0, 255):
raise ValueError( MSG_BAD_COLOR_SEQ)
c.append( ord(i))
stride = mupdf.fz_pixmap_stride( pm)
i = stride * y + n * x
for j in range(n):
pm.m_internal.samples[i + j] = c[j]
def set_rect(self, bbox, color):
"""Set color of all pixels in bbox."""
pm = self.this
n = pm.n()
c = []
for j in range(n):
i = color[j]
if not _INRANGE(i, 0, 255):
raise ValueError( MSG_BAD_COLOR_SEQ)
c.append(i)
bbox = JM_irect_from_py(bbox)
i = JM_fill_pixmap_rect_with_color(pm, c, bbox)
rc = bool(i)
return rc
def shrink(self, factor):
"""Divide width and height by 2**factor.
E.g. factor=1 shrinks to 25% of original size (in place)."""
if factor < 1:
JM_Warning("ignoring shrink factor < 1")
return
mupdf.fz_subsample_pixmap( self.this, factor)
@property
def size(self):
"""Pixmap size."""
pm = self.this
return pm.n() * pm.w() * pm.h()
@property
def stride(self):
"""Length of one image line (width * n)."""
return self.this.stride()
def tint_with(self, black, white):
"""Tint colors with modifiers for black and white."""
if not self.colorspace or self.colorspace.n > 3:
print("warning: colorspace invalid for function")
return
return mupdf.fz_tint_pixmap( self.this, black, white)
@property
def w(self):
"""The width."""
return mupdf.fz_pixmap_width(self.this)
def warp(self, quad, width, height):
"""Return pixmap from a warped quad."""
if not quad.is_convex: raise ValueError("quad must be convex")
q = JM_quad_from_py(quad)
points = [ q.ul, q.ur, q.lr, q.ll]
dst = mupdf.fz_warp_pixmap( self.this, points, width, height)
return Pixmap( dst)
@property
def x(self):
"""x component of Pixmap origin."""
return mupdf.fz_pixmap_x(self.this)
@property
def xres(self):
"""Resolution in x direction."""
return self.this.xres()
@property
def y(self):
"""y component of Pixmap origin."""
return mupdf.fz_pixmap_y(self.this)
@property
def yres(self):
"""Resolution in y direction."""
return mupdf.fz_pixmap_width(self.this)
width = w
height = h
del Point
class Point:
def __abs__(self):
return math.sqrt(self.x * self.x + self.y * self.y)
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 __bool__(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 __getitem__(self, i):
return (self.x, self.y)[i]
def __hash__(self):
return hash(tuple(self))
def __init__(self, *args, x=None, y=None):
'''
Point() - all zeros
Point(x, y)
Point(Point) - new copy
Point(sequence) - from 'sequence'
Explicit keyword args x, y override earlier settings if not None.
'''
if not args:
self.x = 0.0
self.y = 0.0
elif len(args) > 2:
raise ValueError("Point: bad seq len")
elif len(args) == 2:
self.x = float(args[0])
self.y = float(args[1])
elif len(args) == 1:
l = args[0]
if isinstance(l, (mupdf.FzPoint, mupdf.fz_point)):
self.x = l.x
self.y = l.y
else:
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])
else:
raise ValueError("Point: bad seq len")
if x is not None: self.x = x
if y is not None: self.y = y
def __len__(self):
return 2
def __mul__(self, m):
if hasattr(m, "__float__"):
return Point(self.x * m, self.y * m)
p = Point(self)
return p.transform(m)
def __neg__(self):
return Point(-self.x, -self.y)
def __nonzero__(self):
return not (max(self) == min(self) == 0)
def __pos__(self):
return Point(self)
def __repr__(self):
return "Point" + str(tuple(self))
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 __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 __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)
@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
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 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)
__div__ = __truediv__
norm = __abs__
class Quad:
def __abs__(self):
if self.is_empty:
return 0.0
return abs(self.ul - self.ur) * abs(self.ul - self.ll)
def __add__(self, q):
if hasattr(q, "__float__"):
return Quad(self.ul + q, self.ur + q, self.ll + q, self.lr + q)
if len(q) != 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 __bool__(self):
return not self.is_empty
def __contains__(self, x):
try:
l = x.__len__()
except Exception:
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 __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 __getitem__(self, i):
return (self.ul, self.ur, self.ll, self.lr)[i]
def __hash__(self):
return hash(tuple(self))
def __init__(self, *args, ul=None, ur=None, ll=None, lr=None):
'''
Quad() - all zero points
Quad(ul, ur, ll, lr)
Quad(quad) - new copy
Quad(sequence) - from 'sequence'
Explicit keyword args ul, ur, ll, lr override earlier settings if not
None.
'''
if not args:
self.ul = self.ur = self.ll = self.lr = Point()
elif len(args) > 4:
raise ValueError("Quad: bad seq len")
elif len(args) == 4:
self.ul, self.ur, self.ll, self.lr = map(Point, args)
elif len(args) == 1:
l = args[0]
if isinstance(l, mupdf.FzQuad):
self.this = l
self.ul, self.ur, self.ll, self.lr = Point(l.ul), Point(l.ur), Point(l.ll), Point(l.lr)
elif hasattr(l, "__getitem__") is False:
raise ValueError("Quad: bad args")
elif len(l) != 4:
raise ValueError("Quad: bad seq len")
else:
self.ul, self.ur, self.ll, self.lr = map(Point, l)
else:
raise ValueError("Quad: bad args")
if ul is not None: self.ul = Point(ul)
if ur is not None: self.ur = Point(ur)
if ll is not None: self.ll = Point(ll)
if lr is not None: self.lr = Point(lr)
def __len__(self):
return 4
def __mul__(self, m):
q = Quad(self)
q = q.transform(m)
return q
def __neg__(self):
return Quad(-self.ul, -self.ur, -self.ll, -self.lr)
def __nonzero__(self):
return not self.is_empty
def __pos__(self):
return Quad(self)
def __repr__(self):
return "Quad" + str(tuple(self))
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 __sub__(self, q):
if hasattr(q, "__float__"):
return Quad(self.ul - q, self.ur - q, self.ll - q, self.lr - q)
if len(q) != 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
@property
def is_convex(self):
"""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)
p1 = self.ll * m
p2 = self.ur * m
if p1.y * p2.y > 0:
return False
m = planish_line(self.ll, self.ur)
p1 = self.lr * m
p2 = self.ul * m
if p1.y * p2.y > 0:
return False
return True
@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 is_rectangular(self):
"""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:
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
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
@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 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
__div__ = __truediv__
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)))
class Rect:
def __abs__(self):
if self.is_empty or self.is_infinite:
return 0.0
return (self.x1 - self.x0) * (self.y1 - self.y0)
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 __and__(self, x):
if not hasattr(x, "__len__"):
raise ValueError("bad operand 2")
r1 = Rect(x)
r = Rect(self)
return r.intersect(r1)
def __bool__(self):
return not (max(self) == min(self) == 0)
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 Exception:
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 __eq__(self, rect):
if not hasattr(rect, "__len__"):
return False
return len(rect) == 4 and bool(self - rect) is False
def __getitem__(self, i):
return (self.x0, self.y0, self.x1, self.y1)[i]
def __hash__(self):
return hash(tuple(self))
def __init__(self, *args, p0=None, p1=None, x0=None, y0=None, x1=None, y1=None):
"""
Rect() - all zeros
Rect(x0, y0, x1, y1)
Rect(top-left, x1, y1)
Rect(x0, y0, bottom-right)
Rect(top-left, bottom-right)
Rect(Rect or IRect) - new copy
Rect(sequence) - from 'sequence'
Explicit keyword args p0, p1, x0, y0, x1, y1 override earlier settings
if not None.
"""
x0, y0, x1, y1 = util_make_rect( *args, p0=p0, p1=p1, x0=x0, y0=y0, x1=x1, y1=y1)
self.x0 = float( x0)
self.y0 = float( y0)
self.x1 = float( x1)
self.y1 = float( y1)
def __len__(self):
return 4
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 __neg__(self):
return Rect(-self.x0, -self.y0, -self.x1, -self.y1)
def __nonzero__(self):
return not (max(self) == min(self) == 0)
def __or__(self, x):
if not hasattr(x, "__len__"):
raise ValueError("bad operand 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 operand 2")
def __pos__(self):
return Rect(self)
def __repr__(self):
return "Rect" + str(tuple(self))
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 __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 __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(f"Matrix not invertible: {m}")
r = Rect(self)
r = r.transform(im)
return r
@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)
def contains(self, x):
"""Check if containing point-like or rect-like x."""
return self.__contains__(x)
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 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
@property
def is_empty(self):
"""True if rectangle area is empty."""
return self.x0 >= self.x1 or 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 is_valid(self):
"""True if rectangle is valid."""
return self.x0 <= self.x1 and self.y0 <= self.y1
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 norm(self):
return math.sqrt(sum([c*c for c in self]))
def normalize(self):
"""Replace rectangle with its finite 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 quad(self):
"""Return Quad version of rectangle."""
return Quad(self.tl, self.tr, self.bl, self.br)
def round(self):
"""Return the IRect."""
return IRect(util_round_rect(self))
@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)
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 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
__div__ = __truediv__
bl = bottom_left
br = bottom_right
height = property(lambda self: abs(self.y1 - self.y0))
irect = property(round)
tl = top_left
tr = top_right
width = property(lambda self: abs(self.x1 - self.x0))
class Shape:
"""Create a new shape."""
def __init__(self, page: Page):
CheckParent(page)
self.page = page
self.doc = page.parent
if not self.doc.is_pdf:
raise ValueError("not a PDF")
self.height = page.mediabox_size.y
self.width = page.mediabox_size.x
self.x = page.cropbox_position.x
self.y = page.cropbox_position.y
self.pctm = page.transformation_matrix
self.ipctm = ~self.pctm
self.draw_cont = ""
self.text_cont = ""
self.totalcont = ""
self.lastPoint = None
self.rect = None
def commit(self, overlay: bool = True) -> None:
"""
Update the page's /Contents object with Shape data. The argument
controls whether data appear in foreground (default) or background.
"""
CheckParent(self.page)
self.totalcont += self.text_cont
self.totalcont = self.totalcont.encode()
if self.totalcont != b"":
xref = TOOLS._insert_contents(self.page, b" ", overlay)
mupdf.pdf_update_stream(self.doc, xref, self.totalcont)
self.lastPoint = None
self.rect = None
self.draw_cont = ""
self.text_cont = ""
self.totalcont = ""
return
def draw_bezier(
self,
p1: point_like,
p2: point_like,
p3: point_like,
p4: point_like,
):
"""Draw a standard cubic Bezier curve."""
p1 = Point(p1)
p2 = Point(p2)
p3 = Point(p3)
p4 = Point(p4)
if not (self.lastPoint == p1):
self.draw_cont += "%g %g m\n" % JM_TUPLE(p1 * self.ipctm)
self.draw_cont += "%g %g %g %g %g %g c\n" % JM_TUPLE(
list(p2 * self.ipctm) + list(p3 * self.ipctm) + list(p4 * self.ipctm)
)
self.updateRect(p1)
self.updateRect(p2)
self.updateRect(p3)
self.updateRect(p4)
self.lastPoint = p4
return self.lastPoint
def draw_circle(self, center: point_like, radius: float):
"""Draw a circle given its center and radius."""
if not radius > EPSILON:
raise ValueError("radius must be postive")
center = Point(center)
p1 = center - (radius, 0)
return self.draw_sector(center, p1, 360, fullSector=False)
def draw_curve(
self,
p1: point_like,
p2: point_like,
p3: point_like,
):
"""Draw a curve between points using one control point."""
kappa = 0.55228474983
p1 = Point(p1)
p2 = Point(p2)
p3 = Point(p3)
k1 = p1 + (p2 - p1) * kappa
k2 = p3 + (p2 - p3) * kappa
return self.draw_bezier(p1, k1, k2, p3)
def draw_line(self, p1: point_like, p2: point_like):
"""Draw a line between two points."""
p1 = Point(p1)
p2 = Point(p2)
if not (self.lastPoint == p1):
self.draw_cont += "%g %g m\n" % JM_TUPLE(p1 * self.ipctm)
self.lastPoint = p1
self.updateRect(p1)
self.draw_cont += "%g %g l\n" % JM_TUPLE(p2 * self.ipctm)
self.updateRect(p2)
self.lastPoint = p2
return self.lastPoint
def draw_oval(self, tetra: typing.Union[quad_like, rect_like]):
"""Draw an ellipse inside a tetrapod."""
if len(tetra) != 4:
raise ValueError("invalid arg length")
if hasattr(tetra[0], "__float__"):
q = Rect(tetra).quad
else:
q = Quad(tetra)
mt = q.ul + (q.ur - q.ul) * 0.5
mr = q.ur + (q.lr - q.ur) * 0.5
mb = q.ll + (q.lr - q.ll) * 0.5
ml = q.ul + (q.ll - q.ul) * 0.5
if not (self.lastPoint == ml):
self.draw_cont += "%g %g m\n" % JM_TUPLE(ml * self.ipctm)
self.lastPoint = ml
self.draw_curve(ml, q.ll, mb)
self.draw_curve(mb, q.lr, mr)
self.draw_curve(mr, q.ur, mt)
self.draw_curve(mt, q.ul, ml)
self.updateRect(q.rect)
self.lastPoint = ml
return self.lastPoint
def draw_polyline(self, points: list):
"""Draw several connected line segments."""
for i, p in enumerate(points):
if i == 0:
if not (self.lastPoint == Point(p)):
self.draw_cont += "%g %g m\n" % JM_TUPLE(Point(p) * self.ipctm)
self.lastPoint = Point(p)
else:
self.draw_cont += "%g %g l\n" % JM_TUPLE(Point(p) * self.ipctm)
self.updateRect(p)
self.lastPoint = Point(points[-1])
return self.lastPoint
def draw_quad(self, quad: quad_like):
"""Draw a Quad."""
q = Quad(quad)
return self.draw_polyline([q.ul, q.ll, q.lr, q.ur, q.ul])
def draw_rect(self, rect: rect_like):
"""Draw a rectangle."""
r = Rect(rect)
self.draw_cont += "%g %g %g %g re\n" % JM_TUPLE(
list(r.bl * self.ipctm) + [r.width, r.height]
)
self.updateRect(r)
self.lastPoint = r.tl
return self.lastPoint
def draw_sector(
self,
center: point_like,
point: point_like,
beta: float,
fullSector: bool = True,
):
"""Draw a circle sector."""
center = Point(center)
point = Point(point)
l3 = "%g %g m\n"
l4 = "%g %g %g %g %g %g c\n"
l5 = "%g %g l\n"
betar = math.radians(-beta)
w360 = math.radians(math.copysign(360, betar)) * (-1)
w90 = math.radians(math.copysign(90, betar))
w45 = w90 / 2
while abs(betar) > 2 * math.pi:
betar += w360
if not (self.lastPoint == point):
self.draw_cont += l3 % JM_TUPLE(point * self.ipctm)
self.lastPoint = point
Q = Point(0, 0)
C = center
P = point
S = P - C
rad = abs(S)
if not rad > EPSILON:
raise ValueError("radius must be positive")
alfa = self.horizontal_angle(center, point)
while abs(betar) > abs(w90):
q1 = C.x + math.cos(alfa + w90) * rad
q2 = C.y + math.sin(alfa + w90) * rad
Q = Point(q1, q2)
r1 = C.x + math.cos(alfa + w45) * rad / math.cos(w45)
r2 = C.y + math.sin(alfa + w45) * rad / math.cos(w45)
R = Point(r1, r2)
kappah = (1 - math.cos(w45)) * 4 / 3 / abs(R - Q)
kappa = kappah * abs(P - Q)
cp1 = P + (R - P) * kappa
cp2 = Q + (R - Q) * kappa
self.draw_cont += l4 % JM_TUPLE(
list(cp1 * self.ipctm) + list(cp2 * self.ipctm) + list(Q * self.ipctm)
)
betar -= w90
alfa += w90
P = Q
if abs(betar) > 1e-3:
beta2 = betar / 2
q1 = C.x + math.cos(alfa + betar) * rad
q2 = C.y + math.sin(alfa + betar) * rad
Q = Point(q1, q2)
r1 = C.x + math.cos(alfa + beta2) * rad / math.cos(beta2)
r2 = C.y + math.sin(alfa + beta2) * rad / math.cos(beta2)
R = Point(r1, r2)
kappah = (1 - math.cos(beta2)) * 4 / 3 / abs(R - Q)
kappa = kappah * abs(P - Q) / (1 - math.cos(betar))
cp1 = P + (R - P) * kappa
cp2 = Q + (R - Q) * kappa
self.draw_cont += l4 % JM_TUPLE(
list(cp1 * self.ipctm) + list(cp2 * self.ipctm) + list(Q * self.ipctm)
)
if fullSector:
self.draw_cont += l3 % JM_TUPLE(point * self.ipctm)
self.draw_cont += l5 % JM_TUPLE(center * self.ipctm)
self.draw_cont += l5 % JM_TUPLE(Q * self.ipctm)
self.lastPoint = Q
return self.lastPoint
def draw_squiggle(
self,
p1: point_like,
p2: point_like,
breadth=2,
):
"""Draw a squiggly line from p1 to p2."""
p1 = Point(p1)
p2 = Point(p2)
S = p2 - p1
rad = abs(S)
cnt = 4 * int(round(rad / (4 * breadth), 0))
if cnt < 4:
raise ValueError("points too close")
mb = rad / cnt
matrix = Matrix(TOOLS._hor_matrix(p1, p2))
i_mat = ~matrix
k = 2.4142135623765633
points = []
for i in range(1, cnt):
if i % 4 == 1:
p = Point(i, -k) * mb
elif i % 4 == 3:
p = Point(i, k) * mb
else:
p = Point(i, 0) * mb
points.append(p * i_mat)
points = [p1] + points + [p2]
cnt = len(points)
i = 0
while i + 2 < cnt:
self.draw_curve(points[i], points[i + 1], points[i + 2])
i += 2
return p2
def draw_zigzag(
self,
p1: point_like,
p2: point_like,
breadth: float = 2,
):
"""Draw a zig-zagged line from p1 to p2."""
p1 = Point(p1)
p2 = Point(p2)
S = p2 - p1
rad = abs(S)
cnt = 4 * int(round(rad / (4 * breadth), 0))
if cnt < 4:
raise ValueError("points too close")
mb = rad / cnt
matrix = Matrix(TOOLS._hor_matrix(p1, p2))
i_mat = ~matrix
points = []
for i in range(1, cnt):
if i % 4 == 1:
p = Point(i, -1) * mb
elif i % 4 == 3:
p = Point(i, 1) * mb
else:
continue
points.append(p * i_mat)
self.draw_polyline([p1] + points + [p2])
return p2
def finish(
self,
width: float = 1,
color: OptSeq = (0,),
fill: OptSeq = None,
lineCap: int = 0,
lineJoin: int = 0,
dashes: OptStr = None,
even_odd: bool = False,
morph: OptSeq = None,
closePath: bool = True,
fill_opacity: float = 1,
stroke_opacity: float = 1,
oc: int = 0,
) -> None:
"""Finish the current drawing segment.
Notes:
Apply colors, opacity, dashes, line style and width, or
morphing. Also whether to close the path
by connecting last to first point.
"""
if self.draw_cont == "":
return
if width == 0:
color = None
elif color is None:
width = 0
color_str = ColorCode(color, "c")
fill_str = ColorCode(fill, "f")
optcont = self.page._get_optional_content(oc)
if optcont is not None:
self.draw_cont = "/OC /%s BDC\n" % optcont + self.draw_cont
emc = "EMC\n"
else:
emc = ""
alpha = self.page._set_opacity(CA=stroke_opacity, ca=fill_opacity)
if alpha is not None:
self.draw_cont = "/%s gs\n" % alpha + self.draw_cont
if width != 1 and width != 0:
self.draw_cont += "%g w\n" % width
if lineCap != 0:
self.draw_cont = "%i J\n" % lineCap + self.draw_cont
if lineJoin != 0:
self.draw_cont = "%i j\n" % lineJoin + self.draw_cont
if dashes not in (None, "", "[] 0"):
self.draw_cont = "%s d\n" % dashes + self.draw_cont
if closePath:
self.draw_cont += "h\n"
self.lastPoint = None
if color is not None:
self.draw_cont += color_str
if fill is not None:
self.draw_cont += fill_str
if color is not None:
if not even_odd:
self.draw_cont += "B\n"
else:
self.draw_cont += "B*\n"
else:
if not even_odd:
self.draw_cont += "f\n"
else:
self.draw_cont += "f*\n"
else:
self.draw_cont += "S\n"
self.draw_cont += emc
if CheckMorph(morph):
m1 = Matrix(
1, 0, 0, 1, morph[0].x + self.x, self.height - morph[0].y - self.y
)
mat = ~m1 * morph[1] * m1
self.draw_cont = "%g %g %g %g %g %g cm\n" % JM_TUPLE(mat) + self.draw_cont
self.totalcont += "\nq\n" + self.draw_cont + "Q\n"
self.draw_cont = ""
self.lastPoint = None
return
@staticmethod
def horizontal_angle(C, P):
"""Return the angle to the horizontal for the connection from C to P.
This uses the arcus sine function and resolves its inherent ambiguity by
looking up in which quadrant vector S = P - C is located.
"""
S = Point(P - C).unit
alfa = math.asin(abs(S.y))
if S.x < 0:
if S.y <= 0:
alfa = -(math.pi - alfa)
else:
alfa = math.pi - alfa
else:
if S.y >= 0:
pass
else:
alfa = -alfa
return alfa
def insert_text(
self,
point: point_like,
buffer_: typing.Union[str, list],
fontsize: float = 11,
lineheight: OptFloat = None,
fontname: str = "helv",
fontfile: OptStr = None,
set_simple: bool = 0,
encoding: int = 0,
color: OptSeq = None,
fill: OptSeq = None,
render_mode: int = 0,
border_width: float = 1,
rotate: int = 0,
morph: OptSeq = None,
stroke_opacity: float = 1,
fill_opacity: float = 1,
oc: int = 0,
) -> int:
if not bool(buffer_):
return 0
if type(buffer_) not in (list, tuple):
text = buffer_.splitlines()
else:
text = buffer_
if not len(text) > 0:
return 0
point = Point(point)
try:
maxcode = max([ord(c) for c in " ".join(text)])
except Exception:
exception_info()
return 0
fname = fontname
if fname.startswith("/"):
fname = fname[1:]
xref = self.page.insert_font(
fontname=fname,
fontfile=fontfile,
encoding=encoding,
set_simple=set_simple,
)
fontinfo = CheckFontInfo(self.doc, xref)
fontdict = fontinfo[1]
ordering = fontdict["ordering"]
simple = fontdict["simple"]
bfname = fontdict["name"]
ascender = fontdict["ascender"]
descender = fontdict["descender"]
if lineheight:
lheight = fontsize * lineheight
elif ascender - descender <= 1:
lheight = fontsize * 1.2
else:
lheight = fontsize * (ascender - descender)
if maxcode > 255:
glyphs = self.doc.get_char_widths(xref, maxcode + 1)
else:
glyphs = fontdict["glyphs"]
tab = []
for t in text:
if simple and bfname not in ("Symbol", "ZapfDingbats"):
g = None
else:
g = glyphs
tab.append(getTJstr(t, g, simple, ordering))
text = tab
color_str = ColorCode(color, "c")
fill_str = ColorCode(fill, "f")
if not fill and render_mode == 0:
fill = color
fill_str = ColorCode(color, "f")
morphing = CheckMorph(morph)
rot = rotate
if rot % 90 != 0:
raise ValueError("bad rotate value")
while rot < 0:
rot += 360
rot = rot % 360
templ1 = "\nq\n%s%sBT\n%s1 0 0 1 %g %g Tm\n/%s %g Tf "
templ2 = "TJ\n0 -%g TD\n"
cmp90 = "0 1 -1 0 0 0 cm\n"
cmm90 = "0 -1 1 0 0 0 cm\n"
cm180 = "-1 0 0 -1 0 0 cm\n"
height = self.height
width = self.width
if morphing:
m1 = Matrix(1, 0, 0, 1, morph[0].x + self.x, height - morph[0].y - self.y)
mat = ~m1 * morph[1] * m1
cm = "%g %g %g %g %g %g cm\n" % JM_TUPLE(mat)
else:
cm = ""
top = height - point.y - self.y
left = point.x + self.x
space = top
if rot == 90:
left = height - point.y - self.y
top = -point.x - self.x
cm += cmp90
space = width - abs(top)
elif rot == 270:
left = -height + point.y + self.y
top = point.x + self.x
cm += cmm90
space = abs(top)
elif rot == 180:
left = -point.x - self.x
top = -height + point.y + self.y
cm += cm180
space = abs(point.y + self.y)
optcont = self.page._get_optional_content(oc)
if optcont is not None:
bdc = "/OC /%s BDC\n" % optcont
emc = "EMC\n"
else:
bdc = emc = ""
alpha = self.page._set_opacity(CA=stroke_opacity, ca=fill_opacity)
if alpha is None:
alpha = ""
else:
alpha = "/%s gs\n" % alpha
nres = templ1 % (bdc, alpha, cm, left, top, fname, fontsize)
if render_mode > 0:
nres += "%i Tr " % render_mode
if border_width != 1:
nres += "%g w " % border_width
if color is not None:
nres += color_str
if fill is not None:
nres += fill_str
nres += text[0]
nlines = 1
if len(text) > 1:
nres += templ2 % lheight
else:
nres += templ2[:2]
for i in range(1, len(text)):
if space < lheight:
break
if i > 1:
nres += "\nT* "
nres += text[i] + templ2[:2]
space -= lheight
nlines += 1
nres += "\nET\n%sQ\n" % emc
self.text_cont += nres
return nlines
def insert_textbox(
self,
rect: rect_like,
buffer_: typing.Union[str, list],
fontname: OptStr = "helv",
fontfile: OptStr = None,
fontsize: float = 11,
lineheight: OptFloat = None,
set_simple: bool = 0,
encoding: int = 0,
color: OptSeq = None,
fill: OptSeq = None,
expandtabs: int = 1,
border_width: float = 1,
align: int = 0,
render_mode: int = 0,
rotate: int = 0,
morph: OptSeq = None,
stroke_opacity: float = 1,
fill_opacity: float = 1,
oc: int = 0,
) -> float:
"""Insert text into a given rectangle.
Args:
rect -- the textbox to fill
buffer_ -- text to be inserted
fontname -- a Base-14 font, font name or '/name'
fontfile -- name of a font file
fontsize -- font size
lineheight -- overwrite the font property
color -- RGB stroke color triple
fill -- RGB fill color triple
render_mode -- text rendering control
border_width -- thickness of glyph borders
expandtabs -- handles tabulators with string function
align -- left, center, right, justified
rotate -- 0, 90, 180, or 270 degrees
morph -- morph box with a matrix and a fixpoint
Returns:
unused or deficit rectangle area (float)
"""
rect = Rect(rect)
if rect.is_empty or rect.is_infinite:
raise ValueError("text box must be finite and not empty")
color_str = ColorCode(color, "c")
fill_str = ColorCode(fill, "f")
if fill is None and render_mode == 0:
fill = color
fill_str = ColorCode(color, "f")
optcont = self.page._get_optional_content(oc)
if optcont is not None:
bdc = "/OC /%s BDC\n" % optcont
emc = "EMC\n"
else:
bdc = emc = ""
alpha = self.page._set_opacity(CA=stroke_opacity, ca=fill_opacity)
if alpha is None:
alpha = ""
else:
alpha = "/%s gs\n" % alpha
if rotate % 90 != 0:
raise ValueError("rotate must be multiple of 90")
rot = rotate
while rot < 0:
rot += 360
rot = rot % 360
if not bool(buffer_):
return rect.height if rot in (0, 180) else rect.width
cmp90 = "0 1 -1 0 0 0 cm\n"
cmm90 = "0 -1 1 0 0 0 cm\n"
cm180 = "-1 0 0 -1 0 0 cm\n"
height = self.height
fname = fontname
if fname.startswith("/"):
fname = fname[1:]
xref = self.page.insert_font(
fontname=fname, fontfile=fontfile, encoding=encoding, set_simple=set_simple
)
fontinfo = CheckFontInfo(self.doc, xref)
fontdict = fontinfo[1]
ordering = fontdict["ordering"]
simple = fontdict["simple"]
glyphs = fontdict["glyphs"]
bfname = fontdict["name"]
ascender = fontdict["ascender"]
descender = fontdict["descender"]
if lineheight:
lheight_factor = lineheight
elif ascender - descender <= 1:
lheight_factor = 1.2
else:
lheight_factor = ascender - descender
lheight = fontsize * lheight_factor
if type(buffer_) in (list, tuple):
t0 = "\n".join(buffer_)
else:
t0 = buffer_
maxcode = max([ord(c) for c in t0])
if simple and maxcode > 255:
t0 = "".join([c if ord(c) < 256 else "?" for c in t0])
t0 = t0.splitlines()
glyphs = self.doc.get_char_widths(xref, maxcode + 1)
if simple and bfname not in ("Symbol", "ZapfDingbats"):
tj_glyphs = None
else:
tj_glyphs = glyphs
def pixlen(x):
"""Calculate pixel length of x."""
if ordering < 0:
return sum([glyphs[ord(c)][1] for c in x]) * fontsize
else:
return len(x) * fontsize
if ordering < 0:
blen = glyphs[32][1] * fontsize
else:
blen = fontsize
text = ""
if CheckMorph(morph):
m1 = Matrix(
1, 0, 0, 1, morph[0].x + self.x, self.height - morph[0].y - self.y
)
mat = ~m1 * morph[1] * m1
cm = "%g %g %g %g %g %g cm\n" % JM_TUPLE(mat)
else:
cm = ""
progr = 1
c_pnt = Point(0, fontsize * ascender)
if rot == 0:
point = rect.tl + c_pnt
pos = point.y + self.y
maxwidth = rect.width
maxpos = rect.y1 + self.y
elif rot == 90:
c_pnt = Point(fontsize * ascender, 0)
point = rect.bl + c_pnt
pos = point.x + self.x
maxwidth = rect.height
maxpos = rect.x1 + self.x
cm += cmp90
elif rot == 180:
c_pnt = -Point(0, fontsize * ascender)
point = rect.br + c_pnt
pos = point.y + self.y
maxwidth = rect.width
progr = -1
maxpos = rect.y0 + self.y
cm += cm180
else:
c_pnt = -Point(fontsize * ascender, 0)
point = rect.tr + c_pnt
pos = point.x + self.x
maxwidth = rect.height
progr = -1
maxpos = rect.x0 + self.x
cm += cmm90
just_tab = []
for i, line in enumerate(t0):
line_t = line.expandtabs(expandtabs).split(" ")
lbuff = ""
rest = maxwidth
for word in line_t:
pl_w = pixlen(word)
if rest >= pl_w:
lbuff += word + " "
rest -= pl_w + blen
continue
if len(lbuff) > 0:
lbuff = lbuff.rstrip() + "\n"
text += lbuff
pos += lheight * progr
just_tab.append(True)
lbuff = ""
rest = maxwidth
if pl_w <= maxwidth:
lbuff = word + " "
rest = maxwidth - pl_w - blen
continue
if len(just_tab) > 0:
just_tab[-1] = False
for c in word:
if pixlen(lbuff) <= maxwidth - pixlen(c):
lbuff += c
else:
lbuff += "\n"
text += lbuff
pos += lheight * progr
just_tab.append(False)
lbuff = c
lbuff += " "
rest = maxwidth - pixlen(lbuff)
if lbuff != "":
text += lbuff.rstrip()
just_tab.append(False)
if i < len(t0) - 1:
text += "\n"
pos += lheight * progr
more = (pos - maxpos) * progr
if more > EPSILON:
return (-1) * more
more = abs(more)
if more < EPSILON:
more = 0
nres = "\nq\n%s%sBT\n" % (bdc, alpha) + cm
templ = "1 0 0 1 %g %g Tm /%s %g Tf "
text_t = text.splitlines()
just_tab[-1] = False
for i, t in enumerate(text_t):
pl = maxwidth - pixlen(t)
pnt = point + c_pnt * (i * lheight_factor)
if align == 1:
if rot in (0, 180):
pnt = pnt + Point(pl / 2, 0) * progr
else:
pnt = pnt - Point(0, pl / 2) * progr
elif align == 2:
if rot in (0, 180):
pnt = pnt + Point(pl, 0) * progr
else:
pnt = pnt - Point(0, pl) * progr
elif align == 3:
spaces = t.count(" ")
if spaces > 0 and just_tab[i]:
spacing = pl / spaces
else:
spacing = 0
top = height - pnt.y - self.y
left = pnt.x + self.x
if rot == 90:
left = height - pnt.y - self.y
top = -pnt.x - self.x
elif rot == 270:
left = -height + pnt.y + self.y
top = pnt.x + self.x
elif rot == 180:
left = -pnt.x - self.x
top = -height + pnt.y + self.y
nres += templ % (left, top, fname, fontsize)
if render_mode > 0:
nres += "%i Tr " % render_mode
if align == 3:
nres += "%g Tw " % spacing
if color is not None:
nres += color_str
if fill is not None:
nres += fill_str
if border_width != 1:
nres += "%g w " % border_width
nres += "%sTJ\n" % getTJstr(t, tj_glyphs, simple, ordering)
nres += "ET\n%sQ\n" % emc
self.text_cont += nres
self.updateRect(rect)
return more
def update_rect(self, x):
if self.rect is None:
if len(x) == 2:
self.rect = Rect(x, x)
else:
self.rect = Rect(x)
else:
if len(x) == 2:
x = Point(x)
self.rect.x0 = min(self.rect.x0, x.x)
self.rect.y0 = min(self.rect.y0, x.y)
self.rect.x1 = max(self.rect.x1, x.x)
self.rect.y1 = max(self.rect.y1, x.y)
else:
x = Rect(x)
self.rect.x0 = min(self.rect.x0, x.x0)
self.rect.y0 = min(self.rect.y0, x.y0)
self.rect.x1 = max(self.rect.x1, x.x1)
self.rect.y1 = max(self.rect.y1, x.y1)
class Story:
def __init__( self, html='', user_css=None, em=12, archive=None):
buffer_ = mupdf.fz_new_buffer_from_copied_data( html.encode('utf-8'))
arch = archive.this if archive else mupdf.FzArchive( None)
if hasattr(mupdf, 'FzStoryS'):
self.this = mupdf.FzStoryS( buffer_, user_css, em, arch)
else:
self.this = mupdf.FzStory( buffer_, user_css, em, arch)
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}"
x.set_attribute("id", id_)
i += 1
x = x.find_next(None, None, None)
@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)
id_to_position = dict()
for position in positions:
if (position.open_close & 1) and position.id:
if position.id in id_to_position:
pass
else:
id_to_position[ position.id] = position
for position_from in positions:
if ((position_from.open_close & 1)
and position_from.href
and position_from.href.startswith("#")
):
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}") from e
if 0:
log(f"add_pdf_links(): making link from:")
log(f"add_pdf_links(): {position_from}")
log(f"add_pdf_links(): to:")
log(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
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 document( self):
dom = mupdf.fz_story_document( self.this)
return Xml( dom)
def draw( self, device, matrix=None):
ctm2 = JM_matrix_from_py( matrix)
dev = device.this if device else mupdf.FzDevice( None)
sys.stdout.flush()
mupdf.fz_draw_story( self.this, dev, ctm2)
def element_positions( self, function, args=None):
'''
Trigger a callback function to record where items have been placed.
'''
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")
def function2( position):
class Position2:
pass
position2 = Position2()
position2.depth = position.depth
position2.heading = position.heading
position2.id = position.id
position2.rect = JM_py_from_rect(position.rect)
position2.text = position.text
position2.open_close = position.open_close
position2.rect_num = position.rectangle_num
position2.href = position.href
if args:
for k, v in args.items():
setattr( position2, k, v)
function( position2)
mupdf.fz_story_positions( self.this, function2)
def place( self, where):
where = JM_rect_from_py( where)
filled = mupdf.FzRect()
more = mupdf.fz_place_story( self.this, where, filled)
return more, JM_py_from_rect( filled)
def reset( self):
mupdf.fz_reset_story( self.this)
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:
page_num += 1
more, filled = self.place( rect)
if positionfn:
def positionfn2(position):
position.page_num = page_num
positionfn(position)
self.element_positions(positionfn2)
if writer:
if mediabox:
if dev:
if pagefn:
pagefn(page_num, mediabox, 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
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):
positions.append(position)
if stable and positionfn:
positionfn(position)
story.write(
writer if stable else None,
rectfn,
positionfn2,
pagefn,
)
if stable:
break
@staticmethod
def write_stabilized_with_links(contentfn, rectfn, user_css=None, em=12, positionfn=None, pagefn=None, archive=None, add_header_ids=True):
stream = io.BytesIO()
writer = DocumentWriter(stream)
positions = []
def positionfn2(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)
def write_with_links(self, rectfn, positionfn=None, pagefn=None):
stream = io.BytesIO()
writer = DocumentWriter(stream)
positions = []
def positionfn2(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)
class TextPage:
def __init__(self, *args):
if args_match(args, mupdf.FzRect):
mediabox = args[0]
self.this = mupdf.FzStextPage( mediabox)
elif args_match(args, mupdf.FzStextPage):
self.this = args[0]
else:
raise Exception(f'Unrecognised args: {args}')
self.thisown = True
def _extractText(self, format_):
this_tpage = self.this
res = mupdf.fz_new_buffer(1024)
out = mupdf.FzOutput( res)
if format_ == 1:
mupdf.fz_print_stext_page_as_html(out, this_tpage, 0)
elif format_ == 3:
mupdf.fz_print_stext_page_as_xml(out, this_tpage, 0)
elif format_ == 4:
mupdf.fz_print_stext_page_as_xhtml(out, this_tpage, 0)
else:
JM_print_stext_page_as_text(res, this_tpage)
text = JM_EscapeStrFromBuffer(res)
return text
def _getNewBlockList(self, page_dict, raw):
JM_make_textpage_dict(self.this, 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 extractBLOCKS(self):
"""Return a list with text block information."""
if g_use_extra:
return extra.extractBLOCKS(self.this)
block_n = -1
this_tpage = self.this
tp_rect = mupdf.FzRect(this_tpage.m_internal.mediabox)
res = mupdf.fz_new_buffer(1024)
lines = []
for block in this_tpage:
block_n += 1
blockrect = mupdf.FzRect(mupdf.FzRect.Fixed_EMPTY)
if block.m_internal.type == mupdf.FZ_STEXT_BLOCK_TEXT:
mupdf.fz_clear_buffer(res)
line_n = -1
last_char = 0
for line in block:
line_n += 1
linerect = mupdf.FzRect(mupdf.FzRect.Fixed_EMPTY)
for ch in line:
cbbox = JM_char_bbox(line, ch)
if (not JM_rects_overlap(tp_rect, cbbox)
and not mupdf.fz_is_infinite_rect(tp_rect)
):
continue
JM_append_rune(res, ch.m_internal.c)
last_char = ch.m_internal.c
linerect = mupdf.fz_union_rect(linerect, cbbox)
if last_char != 10 and not mupdf.fz_is_empty_rect(linerect):
mupdf.fz_append_byte(res, 10)
blockrect = mupdf.fz_union_rect(blockrect, linerect)
text = JM_EscapeStrFromBuffer(res)
elif (JM_rects_overlap(tp_rect, block.m_internal.bbox)
or mupdf.fz_is_infinite_rect(tp_rect)
):
img = block.i_image()
cs = img.colorspace()
text = "<image: %s, width: %d, height: %d, bpc: %d>" % (
mupdf.fz_colorspace_name(cs),
img.w(), img.h(), img.bpc()
)
blockrect = mupdf.fz_union_rect(blockrect, mupdf.FzRect(block.m_internal.bbox))
if not mupdf.fz_is_empty_rect(blockrect):
litem = (
blockrect.x0,
blockrect.y0,
blockrect.x1,
blockrect.y1,
text,
block_n,
block.m_internal.type,
)
lines.append(litem)
return lines
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 extractHTML(self) -> str:
"""Return page content as a HTML string."""
return self._extractText(1)
def extractIMGINFO(self, hashes=0):
"""Return a list with image meta information."""
block_n = -1
this_tpage = self.this
rc = []
for block in this_tpage:
block_n += 1
if block.m_internal.type == mupdf.FZ_STEXT_BLOCK_TEXT:
continue
img = block.i_image()
if hashes:
r = mupdf.FzIrect(FZ_MIN_INF_RECT, FZ_MIN_INF_RECT, FZ_MAX_INF_RECT, FZ_MAX_INF_RECT)
assert mupdf.fz_is_infinite_irect(r)
m = mupdf.FzMatrix(img.w(), 0, 0, img.h(), 0, 0)
pix, w, h = mupdf.fz_get_pixmap_from_image(img, r, m)
digest = mupdf.fz_md5_pixmap2(pix)
digest = bytes(digest)
cs = mupdf.FzColorspace(mupdf.ll_fz_keep_colorspace(img.m_internal.colorspace))
block_dict = dict()
block_dict[ dictkey_number] = block_n
block_dict[ dictkey_bbox] = JM_py_from_rect(block.m_internal.bbox)
block_dict[ dictkey_matrix] = JM_py_from_matrix(block.i_transform())
block_dict[ dictkey_width] = img.w()
block_dict[ dictkey_height] = img.h()
block_dict[ dictkey_colorspace] = mupdf.fz_colorspace_n(cs)
block_dict[ dictkey_cs_name] = mupdf.fz_colorspace_name(cs)
block_dict[ dictkey_xres] = img.xres()
block_dict[ dictkey_yres] = img.xres()
block_dict[ dictkey_bpc] = img.bpc()
block_dict[ dictkey_size] = mupdf.fz_image_size(img)
if hashes:
block_dict[ "digest"] = digest
rc.append(block_dict)
return rc
def extractJSON(self, cb=None, sort=False) -> str:
"""Return 'extractDICT' converted to JSON format."""
import base64
import 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 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 extractRAWJSON(self, cb=None, sort=False) -> str:
"""Return 'extractRAWDICT' converted to JSON format."""
import base64
import 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 extractSelection(self, pointa, pointb):
a = JM_point_from_py(pointa)
b = JM_point_from_py(pointb)
found = mupdf.fz_copy_selection(self.this, a, b, 0)
return found
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 extractTextbox(self, rect):
this_tpage = self.this
assert isinstance(this_tpage, mupdf.FzStextPage)
area = JM_rect_from_py(rect)
found = JM_copy_rectangle(this_tpage, area)
rc = PyUnicode_DecodeRawUnicodeEscape(found)
return rc
def extractWORDS(self, delimiters=None):
"""Return a list with text word information."""
if g_use_extra:
return extra.extractWORDS(self.this, delimiters)
buflen = 0
block_n = -1
wbbox = mupdf.FzRect(mupdf.FzRect.Fixed_EMPTY)
this_tpage = self.this
tp_rect = mupdf.FzRect(this_tpage.m_internal.mediabox)
lines = None
buff = mupdf.fz_new_buffer(64)
lines = []
for block in this_tpage:
block_n += 1
if block.m_internal.type != mupdf.FZ_STEXT_BLOCK_TEXT:
continue
line_n = -1
for line in block:
line_n += 1
word_n = 0
mupdf.fz_clear_buffer(buff)
buflen = 0
for ch in line:
cbbox = JM_char_bbox(line, ch)
if (not JM_rects_overlap(tp_rect, cbbox)
and not mupdf.fz_is_infinite_rect(tp_rect)
):
continue
word_delimiter = JM_is_word_delimiter(ch.m_internal.c, delimiters)
if word_delimiter:
if buflen == 0:
continue
if not mupdf.fz_is_empty_rect(wbbox):
word_n, wbbox = JM_append_word(lines, buff, wbbox, block_n, line_n, word_n)
mupdf.fz_clear_buffer(buff)
buflen = 0
continue
JM_append_rune(buff, ch.m_internal.c)
buflen += 1
wbbox = mupdf.fz_union_rect(wbbox, JM_char_bbox(line, ch))
if buflen and not mupdf.fz_is_empty_rect(wbbox):
word_n, wbbox = JM_append_word(lines, buff, wbbox, block_n, line_n, word_n)
buflen = 0
return lines
def extractXHTML(self) -> str:
"""Return page content as a XHTML string."""
return self._extractText(4)
def extractXML(self) -> str:
"""Return page content as a XML string."""
return self._extractText(3)
def poolsize(self):
"""TextPage current poolsize."""
tpage = self.this
pool = mupdf.Pool(tpage.m_internal.pool)
size = mupdf.fz_pool_size( pool)
pool.m_internal = None
return size
@property
def rect(self):
"""Page rectangle."""
this_tpage = self.this
mediabox = this_tpage.m_internal.mediabox
val = JM_py_from_rect(mediabox)
val = Rect(val)
return val
def search(self, needle, hit_max=0, quads=1):
"""Locate 'needle' returning rects or quads."""
val = JM_search_stext_page(self.this, needle)
if not val:
return val
items = len(val)
for i in range(items):
q = Quad(val[i])
if quads:
val[i] = q
else:
val[i] = q.rect
if quads:
return val
i = 0
while i < items - 1:
v1 = val[i]
v2 = val[i + 1]
if v1.y1 != v2.y1 or (v1 & v2).is_empty:
i += 1
continue
val[i] = v1 | v2
del val[i + 1]
items -= 1
return val
extractTEXT = extractText
class TextWriter:
def __init__(self, page_rect, opacity=1, color=None):
"""Stores text spans for later output on compatible PDF pages."""
self.this = mupdf.fz_new_text()
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
@property
def _bbox(self):
val = JM_py_from_rect( mupdf.fz_bound_text( self.this, mupdf.FzStrokeState(None), mupdf.FzMatrix()))
val = Rect(val)
return val
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:
if 0:
log( '{font.this.m_internal.name=}')
log( '{font.this.m_internal.t3matrix=}')
log( '{font.this.m_internal.bbox=}')
log( '{font.this.m_internal.glyph_count=}')
log( '{font.this.m_internal.use_glyph_bbox=}')
log( '{font.this.m_internal.width_count=}')
log( '{font.this.m_internal.width_default=}')
log( '{font.this.m_internal.has_digest=}')
log( 'Unsupported font {font.name=}')
if mupdf_cppyy:
import cppyy
log( f'Unsupported font {cppyy.gbl.mupdf_font_name(font.this.m_internal)=}')
raise ValueError("Unsupported font '%s'." % font.name)
if right_to_left:
text = self.clean_rtl(text)
text = "".join(reversed(text))
right_to_left = 0
lang = mupdf.fz_text_language_from_string(language)
p = JM_point_from_py(pos)
trm = mupdf.fz_make_matrix(fontsize, 0, 0, fontsize, p.x, p.y)
markup_dir = 0
wmode = 0
if small_caps == 0:
trm = mupdf.fz_show_string( self.this, font.this, trm, text, wmode, right_to_left, markup_dir, lang)
else:
trm = JM_show_string_cs( self.this, font.this, trm, text, wmode, right_to_left, markup_dir, lang)
val = JM_py_from_matrix(trm)
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):
lheight = fontsize * 1.2
for c in text:
self.append(pos, c, font=font, fontsize=fontsize,
language=language)
pos.y += lheight
return self.textRect, self.lastPoint
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
words = text.split(" ")
idx = []
for i in range(len(words)):
w = words[i]
if not (len(w) < 2 or max([ord(c) for c in w]) > 255):
words[i] = "".join(reversed(w))
idx.append(i)
idx2 = []
for i in range(len(idx)):
if idx2 == []:
idx2.append(idx[i])
elif idx[i] > idx2[-1] + 1:
if len(idx2) > 1:
words[idx2[0] : idx2[-1] + 1] = reversed(
words[idx2[0] : idx2[-1] + 1]
)
idx2 = [idx[i]]
elif idx[i] == idx2[-1] + 1:
idx2.append(idx[i])
text = " ".join(words)
return text
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 is not 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 is not None and morph is not 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
if 1:
pdfpage = page._pdf_page()
alpha = 1
if opacity >= 0 and opacity < 1:
alpha = opacity
ncol = 1
dev_color = [0, 0, 0, 0]
if color:
ncol, dev_color = JM_color_FromSequence(color)
if ncol == 3:
colorspace = mupdf.fz_device_rgb()
elif ncol == 4:
colorspace = mupdf.fz_device_cmyk()
else:
colorspace = mupdf.fz_device_gray()
ASSERT_PDF(pdfpage)
resources = mupdf.pdf_new_dict(pdfpage.doc(), 5)
contents = mupdf.fz_new_buffer(1024)
dev = mupdf.pdf_new_pdf_device( pdfpage.doc(), mupdf.FzMatrix(), resources, contents)
mupdf.fz_fill_text(
dev,
self.this,
mupdf.FzMatrix(),
colorspace,
dev_color,
alpha,
mupdf.FzColorParams(mupdf.fz_default_color_params),
)
mupdf.fz_close_device( dev)
max_nums = JM_merge_resources( pdfpage, resources)
cont_string = JM_EscapeStrFromBuffer( contents)
result = (max_nums, cont_string)
val = result
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 is not 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
class IRect:
"""
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 __add__(self, p):
return Rect.__add__(self, p).round()
def __and__(self, x):
return Rect.__and__(self, x).round()
def __contains__(self, x):
return Rect.__contains__(self, x)
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 __getitem__(self, i):
return (self.x0, self.y0, self.x1, self.y1)[i]
def __init__(self, *args, p0=None, p1=None, x0=None, y0=None, x1=None, y1=None):
self.x0, self.y0, self.x1, self.y1 = util_make_irect( *args, p0=p0, p1=p1, x0=x0, y0=y0, x1=x1, y1=y1)
def __len__(self):
return 4
def __mul__(self, m):
return Rect.__mul__(self, m).round()
def __neg__(self):
return IRect(-self.x0, -self.y0, -self.x1, -self.y1)
def __or__(self, x):
return Rect.__or__(self, x).round()
def __pos__(self):
return IRect(self)
def __repr__(self):
return "IRect" + str(tuple(self))
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 __sub__(self, p):
return Rect.__sub__(self, p).round()
def __truediv__(self, m):
return Rect.__truediv__(self, m).round()
@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)
@property
def height(self):
return max(0, self.y1 - self.y0)
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."""
return Rect.intersect(self, r).round()
def intersects(self, x):
return Rect.intersects(self, x)
@property
def is_empty(self):
"""True if rectangle area is empty."""
return self.x0 >= self.x1 or 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 is_valid(self):
"""True if rectangle is valid."""
return self.x0 <= self.x1 and self.y0 <= self.y1
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 norm(self):
return math.sqrt(sum([c*c for c in self]))
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 quad(self):
"""Return Quad version of rectangle."""
return Quad(self.tl, self.tr, self.bl, self.br)
@property
def rect(self):
return Rect(self)
@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)
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 transform(self, m):
return Rect.transform(self, m).round()
@property
def width(self):
return max(0, self.x1 - self.x0)
br = bottom_right
bl = bottom_left
tl = top_left
tr = top_right
if 1:
self = sys.modules[__name__]
if 1:
for name, value in mupdf.__dict__.items():
if name.startswith(('PDF_', 'UCDN_SCRIPT_')):
if name.startswith('PDF_ENUM_NAME_'):
pass
else:
setattr(self, name, value)
else:
for name, value in inspect.getmembers(mupdf):
if name.startswith(('PDF_', 'UCDN_SCRIPT_')):
if name.startswith('PDF_ENUM_NAME_'):
pass
else:
setattr(self, name, value)
PDF_SIGNATURE_DEFAULT_APPEARANCE = (0
| mupdf.PDF_SIGNATURE_SHOW_LABELS
| mupdf.PDF_SIGNATURE_SHOW_DN
| mupdf.PDF_SIGNATURE_SHOW_DATE
| mupdf.PDF_SIGNATURE_SHOW_TEXT_NAME
| mupdf.PDF_SIGNATURE_SHOW_GRAPHIC_NAME
| mupdf.PDF_SIGNATURE_SHOW_LOGO
)
assert mupdf.UCDN_EAST_ASIAN_H == 1
assert PDF_TX_FIELD_IS_MULTILINE == mupdf.PDF_TX_FIELD_IS_MULTILINE
assert UCDN_SCRIPT_ADLAM == mupdf.UCDN_SCRIPT_ADLAM
del self
_adobe_glyphs = {}
_adobe_unicodes = {}
AnyType = typing.Any
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
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"
EPSILON = 1e-5
FLT_EPSILON = 1e-5
FZ_MIN_INF_RECT = -0x80000000
FZ_MAX_INF_RECT = 0x7fffff80
JM_annot_id_stem = "fitz"
JM_mupdf_warnings_store = []
JM_mupdf_show_errors = 1
JM_mupdf_show_warnings = 0
PDF_OC_ON = 0
PDF_OC_TOGGLE = 1
PDF_OC_OFF = 2
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
SigFlag_SignaturesExist = 1
SigFlag_AppendOnly = 2
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
TEXT_ALIGN_LEFT = 0
TEXT_ALIGN_CENTER = 1
TEXT_ALIGN_RIGHT = 2
TEXT_ALIGN_JUSTIFY = 3
TEXT_FONT_SUPERSCRIPT = 1
TEXT_FONT_ITALIC = 2
TEXT_FONT_SERIFED = 4
TEXT_FONT_MONOSPACED = 8
TEXT_FONT_BOLD = 16
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
TEXTFLAGS_WORDS = (0
| TEXT_PRESERVE_LIGATURES
| TEXT_PRESERVE_WHITESPACE
| TEXT_MEDIABOX_CLIP
)
TEXTFLAGS_BLOCKS = (0
| TEXT_PRESERVE_LIGATURES
| TEXT_PRESERVE_WHITESPACE
| TEXT_MEDIABOX_CLIP
)
TEXTFLAGS_DICT = (0
| TEXT_PRESERVE_LIGATURES
| TEXT_PRESERVE_WHITESPACE
| TEXT_MEDIABOX_CLIP
| TEXT_PRESERVE_IMAGES
)
TEXTFLAGS_RAWDICT = TEXTFLAGS_DICT
TEXTFLAGS_SEARCH = (0
| TEXT_PRESERVE_LIGATURES
| TEXT_PRESERVE_WHITESPACE
| TEXT_MEDIABOX_CLIP
| TEXT_DEHYPHENATE
)
TEXTFLAGS_HTML = (0
| TEXT_PRESERVE_LIGATURES
| TEXT_PRESERVE_WHITESPACE
| TEXT_MEDIABOX_CLIP
| TEXT_PRESERVE_IMAGES
)
TEXTFLAGS_XHTML = (0
| TEXT_PRESERVE_LIGATURES
| TEXT_PRESERVE_WHITESPACE
| TEXT_MEDIABOX_CLIP
| TEXT_PRESERVE_IMAGES
)
TEXTFLAGS_XML = (0
| TEXT_PRESERVE_LIGATURES
| TEXT_PRESERVE_WHITESPACE
| TEXT_MEDIABOX_CLIP
)
TEXTFLAGS_TEXT = (0
| TEXT_PRESERVE_LIGATURES
| TEXT_PRESERVE_WHITESPACE
| TEXT_MEDIABOX_CLIP
)
TEXT_ENCODING_LATIN = 0
TEXT_ENCODING_GREEK = 1
TEXT_ENCODING_CYRILLIC = 2
TOOLS_JM_UNIQUE_ID = 0
CS_RGB = 1
CS_GRAY = 2
CS_CMYK = 3
PDF_BM_Color = "Color"
PDF_BM_ColorBurn = "ColorBurn"
PDF_BM_ColorDodge = "ColorDodge"
PDF_BM_Darken = "Darken"
PDF_BM_Difference = "Difference"
PDF_BM_Exclusion = "Exclusion"
PDF_BM_HardLight = "HardLight"
PDF_BM_Hue = "Hue"
PDF_BM_Lighten = "Lighten"
PDF_BM_Luminosity = "Luminosity"
PDF_BM_Multiply = "Multiply"
PDF_BM_Normal = "Normal"
PDF_BM_Overlay = "Overlay"
PDF_BM_Saturation = "Saturation"
PDF_BM_Screen = "Screen"
PDF_BM_SoftLight = "Softlight"
TEXT_FONT_SUPERSCRIPT = 1
TEXT_FONT_ITALIC = 2
TEXT_FONT_SERIFED = 4
TEXT_FONT_MONOSPACED = 8
TEXT_FONT_BOLD = 16
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
csRGB = Colorspace(CS_RGB)
csGRAY = Colorspace(CS_GRAY)
csCMYK = Colorspace(CS_CMYK)
dictkey_align = "align"
dictkey_align = "ascender"
dictkey_bbox = "bbox"
dictkey_blocks = "blocks"
dictkey_bpc = "bpc"
dictkey_c = "c"
dictkey_chars = "chars"
dictkey_color = "color"
dictkey_colorspace = "colorspace"
dictkey_content = "content"
dictkey_creationDate = "creationDate"
dictkey_cs_name = "cs-name"
dictkey_da = "da"
dictkey_dashes = "dashes"
dictkey_desc = "desc"
dictkey_desc = "descender"
dictkey_dir = "dir"
dictkey_effect = "effect"
dictkey_ext = "ext"
dictkey_filename = "filename"
dictkey_fill = "fill"
dictkey_flags = "flags"
dictkey_font = "font"
dictkey_glyph = "glyph"
dictkey_height = "height"
dictkey_id = "id"
dictkey_image = "image"
dictkey_items = "items"
dictkey_length = "length"
dictkey_lines = "lines"
dictkey_matrix = "transform"
dictkey_modDate = "modDate"
dictkey_name = "name"
dictkey_number = "number"
dictkey_origin = "origin"
dictkey_rect = "rect"
dictkey_size = "size"
dictkey_smask = "smask"
dictkey_spans = "spans"
dictkey_stroke = "stroke"
dictkey_style = "style"
dictkey_subject = "subject"
dictkey_text = "text"
dictkey_title = "title"
dictkey_type = "type"
dictkey_ufilename = "ufilename"
dictkey_width = "width"
dictkey_wmode = "wmode"
dictkey_xref = "xref"
dictkey_xres = "xres"
dictkey_yres = "yres"
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 = {}
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),
)
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),
)
def _read_samples( pixmap, offset, n):
ret = []
for i in range( n):
ret.append( mupdf.fz_samples_get( pixmap, offset + i))
return bytes( ret)
def _INRANGE(v, low, high):
return low <= v and v <= high
def _remove_dest_range(pdf, numbers):
pagecount = mupdf.pdf_count_pages(pdf)
for i in range(pagecount):
n1 = i
if n1 in numbers:
continue
pageref = mupdf.pdf_lookup_page_obj( pdf, i)
annots = mupdf.pdf_dict_get( pageref, PDF_NAME('Annots'))
if not annots.m_internal:
continue
len_ = mupdf.pdf_array_len(annots)
for j in range(len_ - 1, -1, -1):
o = mupdf.pdf_array_get( annots, j)
if not mupdf.pdf_name_eq( mupdf.pdf_dict_get( o, PDF_NAME('Subtype')), PDF_NAME('Link')):
continue
action = mupdf.pdf_dict_get( o, PDF_NAME('A'))
dest = mupdf.pdf_dict_get( o, PDF_NAME('Dest'))
if action.m_internal:
if not mupdf.pdf_name_eq( mupdf.pdf_dict_get( action, PDF_NAME('S')), PDF_NAME('GoTo')):
continue
dest = mupdf.pdf_dict_get( action, PDF_NAME('D'))
pno = -1
if mupdf.pdf_is_array( dest):
target = mupdf.pdf_array_get( dest, 0)
pno = mupdf.pdf_lookup_page_number( pdf, target)
elif mupdf.pdf_is_string( dest):
pno, _, _ = mupdf.fz_resolve_link( pdf.super(), mupdf.pdf_to_text_string( dest))
if pno < 0:
continue
n1 = pno
if n1 in numbers:
mupdf.pdf_array_delete( annots, j)
def ASSERT_PDF(cond):
assert isinstance(cond, (mupdf.PdfPage, mupdf.PdfDocument)), f'{type(cond)=} {cond=}'
if not cond.m_internal:
raise Exception(MSG_IS_NO_PDF)
def EMPTY_IRECT():
return IRect(FZ_MAX_INF_RECT, FZ_MAX_INF_RECT, FZ_MIN_INF_RECT, FZ_MIN_INF_RECT)
def EMPTY_QUAD():
return EMPTY_RECT().quad
def EMPTY_RECT():
return Rect(FZ_MAX_INF_RECT, FZ_MAX_INF_RECT, FZ_MIN_INF_RECT, FZ_MIN_INF_RECT)
def ENSURE_OPERATION(pdf):
if not JM_have_operation(pdf):
raise Exception("No journalling operation started")
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 INFINITE_RECT():
return Rect(FZ_MIN_INF_RECT, FZ_MIN_INF_RECT, FZ_MAX_INF_RECT, FZ_MAX_INF_RECT)
def JM_BinFromBuffer(buffer_):
'''
Turn fz_buffer into a Python bytes object
'''
assert isinstance(buffer_, mupdf.FzBuffer)
ret = mupdf.fz_buffer_extract_copy(buffer_)
return ret
def JM_EscapeStrFromStr(c):
if c is None:
return ''
assert isinstance(c, str), f'{type(c)=}'
b = c.encode('utf8', 'surrogateescape')
ret = ''
for bb in b:
ret += chr(bb)
return ret
def JM_BufferFromBytes(stream):
'''
Make fz_buffer from a PyBytes, PyByteArray or io.BytesIO object. If a text
io.BytesIO, we convert to binary by encoding as utf8.
'''
if isinstance(stream, (bytes, bytearray)):
data = stream
elif hasattr(stream, 'getvalue'):
data = stream.getvalue()
if isinstance(data, str):
data = data.encode('utf-8')
if not isinstance(data, (bytes, bytearray)):
raise Exception(f'.getvalue() returned unexpected type: {type(data)}')
else:
return mupdf.FzBuffer()
return mupdf.fz_new_buffer_from_copied_data(data)
def JM_FLOAT_ITEM(obj, idx):
if not PySequence_Check(obj):
return None
return float(obj[idx])
def JM_INT_ITEM(obj, idx):
if idx < len(obj):
temp = obj[idx]
if isinstance(temp, (int, float)):
return 0, temp
return 1, None
def JM_pixmap_from_page(doc, page, ctm, cs, alpha, annots, clip):
'''
Pixmap creation directly using a short-lived displaylist, so we can support
separations.
'''
SPOTS_NONE = 0
SPOTS_OVERPRINT_SIM = 1
SPOTS_FULL = 2
FZ_ENABLE_SPOT_RENDERING = True
if FZ_ENABLE_SPOT_RENDERING:
spots = SPOTS_OVERPRINT_SIM
else:
spots = SPOTS_NONE
seps = None
colorspace = cs
matrix = JM_matrix_from_py(ctm)
rect = mupdf.fz_bound_page(page)
rclip = JM_rect_from_py(clip)
rect = mupdf.fz_intersect_rect(rect, rclip)
rect = mupdf.fz_transform_rect(rect, matrix)
bbox = mupdf.fz_round_rect(rect)
oi = mupdf.fz_document_output_intent(doc)
if oi.m_internal:
if mupdf.fz_colorspace_n(oi) == mupdf.fz_colorspace_n(cs):
colorspace = mupdf.fz_keep_colorspace(oi)
if spots != SPOTS_NONE:
seps = mupdf.fz_page_separations(page)
if seps.m_internal:
n = mupdf.fz_count_separations(seps)
if spots == SPOTS_FULL:
for i in range(n):
mupdf.fz_set_separation_behavior(seps, i, mupdf.FZ_SEPARATION_SPOT)
else:
for i in range(n):
mupdf.fz_set_separation_behavior(seps, i, mupdf.FZ_SEPARATION_COMPOSITE)
elif mupdf.fz_page_uses_overprint(page):
seps = mupdf.fz_new_separations(0)
elif oi.m_internal and mupdf.fz_colorspace_n(oi) != mupdf.fz_colorspace_n(colorspace):
seps = mupdf.fz_new_separations(0)
pix = mupdf.fz_new_pixmap_with_bbox(colorspace, bbox, seps, alpha)
if alpha:
mupdf.fz_clear_pixmap(pix)
else:
mupdf.fz_clear_pixmap_with_value(pix, 0xFF)
dev = mupdf.fz_new_draw_device(matrix, pix)
if annots:
mupdf.fz_run_page(page, dev, mupdf.FzMatrix(), mupdf.FzCookie())
else:
mupdf.fz_run_page_contents(page, dev, mupdf.FzMatrix(), mupdf.FzCookie())
mupdf.fz_close_device(dev)
return pix
def JM_StrAsChar(x):
return x
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 JM_UnicodeFromStr(s):
if s is None:
return ''
if isinstance(s, bytes):
s = s.decode('utf8')
assert isinstance(s, str), f'{type(s)=} {s=}'
return s
def JM_add_annot_id(annot, stem):
'''
Add a unique /NM key to an annotation or widget.
Append a number to 'stem' such that the result is a unique name.
'''
assert isinstance(annot, mupdf.PdfAnnot)
page = mupdf.pdf_annot_page( annot)
annot_obj = mupdf.pdf_annot_obj( annot)
names = JM_get_annot_id_list(page)
i = 0
while 1:
stem_id = f'{JM_annot_id_stem}-{stem}{i}'
if stem_id not in names:
break
i += 1
response = JM_StrAsChar(stem_id)
name = mupdf.pdf_new_string( response, len(response))
mupdf.pdf_dict_puts(annot_obj, "NM", name)
page.doc().m_internal.resynth_required = 0
def JM_add_oc_object(pdf, ref, xref):
'''
Add OC object reference to a dictionary
'''
indobj = mupdf.pdf_new_indirect(pdf, xref, 0)
if not mupdf.pdf_is_dict(indobj):
RAISEPY(MSG_BAD_OC_REF, PyExc_ValueError)
type_ = mupdf.pdf_dict_get(indobj, PDF_NAME('Type'))
if (mupdf.pdf_objcmp(type_, PDF_NAME('OCG')) == 0
or mupdf.pdf_objcmp(type_, PDF_NAME('OCMD')) == 0
):
mupdf.pdf_dict_put(ref, PDF_NAME('OC'), indobj)
else:
RAISEPY(MSG_BAD_OC_REF, PyExc_ValueError)
def JM_annot_border(annot_obj):
dash_py = list()
style = None
width = -1
clouds = -1
obj = None
obj = mupdf.pdf_dict_get( annot_obj, PDF_NAME('Border'))
if mupdf.pdf_is_array( obj):
width = mupdf.pdf_to_real( mupdf.pdf_array_get( obj, 2))
if mupdf.pdf_array_len( obj) == 4:
dash = mupdf.pdf_array_get( obj, 3)
for i in range( mupdf.pdf_array_len( dash)):
val = mupdf.pdf_to_int( mupdf.pdf_array_get( dash, i))
dash_py.append( val)
bs_o = mupdf.pdf_dict_get( annot_obj, PDF_NAME('BS'))
if bs_o.m_internal:
width = mupdf.pdf_to_real( mupdf.pdf_dict_get( bs_o, PDF_NAME('W')))
style = mupdf.pdf_to_name( mupdf.pdf_dict_get( bs_o, PDF_NAME('S')))
if style == '':
style = None
obj = mupdf.pdf_dict_get( bs_o, PDF_NAME('D'))
if obj.m_internal:
for i in range( mupdf.pdf_array_len( obj)):
val = mupdf.pdf_to_int( mupdf.pdf_array_get( obj, i))
dash_py.append( val)
obj = mupdf.pdf_dict_get( annot_obj, PDF_NAME('BE'))
if obj.m_internal:
clouds = mupdf.pdf_to_int( mupdf.pdf_dict_get( obj, PDF_NAME('I')))
res = dict()
res[ dictkey_width] = width
res[ dictkey_dashes] = tuple( dash_py)
res[ dictkey_style] = style
res[ 'clouds'] = clouds
return res
def JM_annot_colors(annot_obj):
res = dict()
bc = list()
fc =list()
o = mupdf.pdf_dict_get(annot_obj, mupdf.PDF_ENUM_NAME_C)
if mupdf.pdf_is_array(o):
n = mupdf.pdf_array_len(o)
for i in range(n):
col = mupdf.pdf_to_real( mupdf.pdf_array_get(o, i))
bc.append(col)
res[dictkey_stroke] = bc
o = mupdf.pdf_dict_gets(annot_obj, "IC")
if mupdf.pdf_is_array(o):
n = mupdf.pdf_array_len(o)
for i in range(n):
col = mupdf.pdf_to_real( mupdf.pdf_array_get(o, i))
fc.append(col)
res[dictkey_fill] = fc
return res
def JM_annot_set_border( border, doc, annot_obj):
assert isinstance(border, dict)
obj = None
dashlen = 0
nwidth = border.get( dictkey_width)
ndashes = border.get( dictkey_dashes)
nstyle = border.get( dictkey_style)
nclouds = border.get( 'clouds')
oborder = JM_annot_border( annot_obj)
mupdf.pdf_dict_del( annot_obj, PDF_NAME('BS'))
mupdf.pdf_dict_del( annot_obj, PDF_NAME('BE'))
mupdf.pdf_dict_del( annot_obj, PDF_NAME('Border'))
if nwidth < 0:
nwidth = oborder.get( dictkey_width)
if ndashes is None:
ndashes = oborder.get( dictkey_dashes)
if nstyle is None:
nstyle = oborder.get( dictkey_style)
if nclouds < 0:
nclouds = oborder.get( "clouds")
if isinstance( ndashes, tuple) and len( ndashes) > 0:
dashlen = len( ndashes)
darr = mupdf.pdf_new_array( doc, dashlen)
for d in ndashes:
mupdf.pdf_array_push_int( darr, d)
mupdf.pdf_dict_putl( annot_obj, darr, PDF_NAME('BS'), PDF_NAME('D'))
mupdf.pdf_dict_putl(
annot_obj,
mupdf.pdf_new_real( nwidth),
PDF_NAME('BS'),
PDF_NAME('W'),
)
if dashlen == 0:
obj = JM_get_border_style( nstyle)
else:
obj = PDF_NAME('D')
mupdf.pdf_dict_putl( annot_obj, obj, PDF_NAME('BS'), PDF_NAME('S'))
if nclouds > 0:
mupdf.pdf_dict_put_dict( annot_obj, PDF_NAME('BE'), 2)
obj = mupdf.pdf_dict_get( annot_obj, PDF_NAME('BE'))
mupdf.pdf_dict_put( obj, PDF_NAME('S'), PDF_NAME('C'))
mupdf.pdf_dict_put_int( obj, PDF_NAME('I'), nclouds)
def make_escape(ch):
if ch == 92:
return "\\u005c"
elif 32 <= ch <= 127 or ch == 10:
return chr(ch)
elif 0xd800 <= ch <= 0xdfff:
return "\\ufffd"
elif ch <= 0xffff:
return "\\u%04x" % ch
else:
return "\\U%08x" % ch
def JM_append_rune(buff, ch):
"""
APPEND non-ascii runes in unicode escape format to fz_buffer.
"""
mupdf.fz_append_string(buff, make_escape(ch))
def JM_append_word(lines, buff, wbbox, block_n, line_n, word_n):
'''
Functions for wordlist output
'''
s = JM_EscapeStrFromBuffer(buff)
litem = (
wbbox.x0,
wbbox.y0,
wbbox.x1,
wbbox.y1,
s,
block_n,
line_n,
word_n,
)
lines.append(litem)
return word_n + 1, mupdf.FzRect(mupdf.FzRect.Fixed_EMPTY)
def JM_add_layer_config( pdf, name, creator, ON):
'''
Add OC configuration to the PDF catalog
'''
ocp = JM_ensure_ocproperties( pdf)
configs = mupdf.pdf_dict_get( ocp, PDF_NAME('Configs'))
if not mupdf.pdf_is_array( configs):
configs = mupdf.pdf_dict_put_array( ocp, PDF_NAME('Configs'), 1)
D = mupdf.pdf_new_dict( pdf, 5)
mupdf.pdf_dict_put_text_string( D, PDF_NAME('Name'), name)
if creator is not None:
mupdf.pdf_dict_put_text_string( D, PDF_NAME('Creator'), creator)
mupdf.pdf_dict_put( D, PDF_NAME('BaseState'), PDF_NAME('OFF'))
onarray = mupdf.pdf_dict_put_array( D, PDF_NAME('ON'), 5)
if not ON:
pass
else:
ocgs = mupdf.pdf_dict_get( ocp, PDF_NAME('OCGs'))
n = len(ON)
for i in range(n):
xref = 0
e, xref = JM_INT_ITEM(ON, i)
if e == 1:
continue
ind = mupdf.pdf_new_indirect( pdf, xref, 0)
if mupdf.pdf_array_contains( ocgs, ind):
mupdf.pdf_array_push( onarray, ind)
mupdf.pdf_array_push( configs, D)
def JM_char_bbox(line, ch):
'''
return rect of char quad
'''
q = JM_char_quad(line, ch)
r = mupdf.fz_rect_from_quad(q)
if not line.m_internal.wmode:
return r
if r.y1 < r.y0 + ch.m_internal.size:
r.y0 = r.y1 - ch.m_internal.size
return r
def JM_char_font_flags(font, line, ch):
flags = detect_super_script(line, ch)
flags += mupdf.fz_font_is_italic(font) * TEXT_FONT_ITALIC
flags += mupdf.fz_font_is_serif(font) * TEXT_FONT_SERIFED
flags += mupdf.fz_font_is_monospaced(font) * TEXT_FONT_MONOSPACED
flags += mupdf.fz_font_is_bold(font) * TEXT_FONT_BOLD
return flags
def JM_char_quad(line, ch):
'''
re-compute char quad if ascender/descender values make no sense
'''
if 1 and g_use_extra:
return mupdf.FzQuad(extra.JM_char_quad( line.m_internal, ch.m_internal))
assert isinstance(line, mupdf.FzStextLine)
assert isinstance(ch, mupdf.FzStextChar)
if g_skip_quad_corrections:
return ch.quad
if line.m_internal.wmode:
return ch.quad
font = mupdf.FzFont(mupdf.ll_fz_keep_font(ch.m_internal.font))
asc = JM_font_ascender(font)
dsc = JM_font_descender(font)
fsize = ch.m_internal.size
asc_dsc = asc - dsc + FLT_EPSILON
if asc_dsc >= 1 and g_small_glyph_heights == 0:
return mupdf.FzQuad(ch.m_internal.quad)
fsize = ch.m_internal.size
bbox = mupdf.fz_font_bbox(font)
fwidth = bbox.x1 - bbox.x0
if asc < 1e-3:
dsc = -0.1
asc = 0.9
asc_dsc = 1.0
if g_small_glyph_heights or asc_dsc < 1:
dsc = dsc / asc_dsc
asc = asc / asc_dsc
asc_dsc = asc - dsc
asc = asc * fsize / asc_dsc
dsc = dsc * fsize / asc_dsc
c = line.m_internal.dir.x
s = line.m_internal.dir.y
trm1 = mupdf.fz_make_matrix(c, -s, s, c, 0, 0)
trm2 = mupdf.fz_make_matrix(c, s, -s, c, 0, 0)
if (c == -1):
trm1.d = 1
trm2.d = 1
xlate1 = mupdf.fz_make_matrix(1, 0, 0, 1, -ch.m_internal.origin.x, -ch.m_internal.origin.y)
xlate2 = mupdf.fz_make_matrix(1, 0, 0, 1, ch.m_internal.origin.x, ch.m_internal.origin.y)
quad = mupdf.fz_transform_quad(mupdf.FzQuad(ch.m_internal.quad), xlate1)
quad = mupdf.fz_transform_quad(quad, trm1)
if c == 1 and quad.ul.y > 0:
quad.ul.y = asc
quad.ur.y = asc
quad.ll.y = dsc
quad.lr.y = dsc
else:
quad.ul.y = -asc
quad.ur.y = -asc
quad.ll.y = -dsc
quad.lr.y = -dsc
if quad.ll.x < 0:
quad.ll.x = 0
quad.ul.x = 0
cwidth = quad.lr.x - quad.ll.x
if cwidth < FLT_EPSILON:
glyph = mupdf.fz_encode_character( font, ch.m_internal.c)
if glyph:
fwidth = mupdf.fz_advance_glyph( font, glyph, line.m_internal.wmode)
quad.lr.x = quad.ll.x + fwidth * fsize
quad.ur.x = quad.lr.x
quad = mupdf.fz_transform_quad(quad, trm2)
quad = mupdf.fz_transform_quad(quad, xlate2)
return quad
def JM_choice_options(annot):
'''
return list of choices for list or combo boxes
'''
annot_obj = mupdf.pdf_annot_obj( annot.this)
def pdf_choice_widget_options( annot, exportval):
optarr = mupdf.pdf_dict_get_inheritable( mupdf.pdf_annot_obj(annot.this), PDF_NAME('Opt'))
n = mupdf.pdf_array_len(optarr)
opts = []
if not n:
return opts
optarr = mupdf.pdf_dict_get(annot_obj, PDF_NAME('Opt'))
for i in range(n):
m = mupdf.pdf_array_len(mupdf.pdf_array_get(optarr, i))
if m == 2:
val = (
mupdf.pdf_to_text_string(mupdf.pdf_array_get(mupdf.pdf_array_get(optarr, i), 0)),
mupdf.pdf_to_text_string(mupdf.pdf_array_get(mupdf.pdf_array_get(optarr, i), 1)),
)
opts.append(val)
else:
val = JM_UnicodeFromStr(mupdf.pdf_to_text_string(mupdf.pdf_array_get(optarr, i)))
opts.append(val)
return opts
opts = pdf_choice_widget_options( annot, 0)
n = len( opts)
if n == 0:
return
optarr = mupdf.pdf_dict_get( annot_obj, PDF_NAME('Opt'))
liste = []
for i in range( n):
m = mupdf.pdf_array_len( mupdf.pdf_array_get( optarr, i))
if m == 2:
val = (
mupdf.pdf_to_text_string( mupdf.pdf_array_get( mupdf.pdf_array_get( optarr, i), 0)),
mupdf.pdf_to_text_string( mupdf.pdf_array_get( mupdf.pdf_array_get( optarr, i), 1)),
)
liste.append( val)
else:
val = mupdf.pdf_to_text_string( mupdf.pdf_array_get( optarr, i))
liste.append( val)
return liste
def JM_clear_pixmap_rect_with_value(dest, value, b):
'''
Clear a pixmap rectangle - my version also supports non-alpha pixmaps
'''
b = mupdf.fz_intersect_irect(b, mupdf.fz_pixmap_bbox(dest))
w = b.x1 - b.x0
y = b.y1 - b.y0
if w <= 0 or y <= 0:
return 0
destspan = dest.stride()
destp = destspan * (b.y0 - dest.y()) + dest.n() * (b.x0 - dest.x())
if mupdf.fz_colorspace_n(dest.colorspace()) == 4:
value = 255 - value
while 1:
s = destp
for x in range(0, w):
mupdf.fz_samples_set(dest, s, 0)
s += 1
mupdf.fz_samples_set(dest, s, 0)
s += 1
mupdf.fz_samples_set(dest, s, 0)
s += 1
mupdf.fz_samples_set(dest, s, value)
s += 1
if dest.alpha():
mupdf.fz_samples_set(dest, s, 255)
s += 1
destp += destspan
if y == 0:
break
y -= 1
return 1
while 1:
s = destp
for x in range(w):
for k in range(dest.n()-1):
mupdf.fz_samples_set(dest, s, value)
s += 1
if dest.alpha():
mupdf.fz_samples_set(dest, s, 255)
s += 1
else:
mupdf.fz_samples_set(dest, s, value)
s += 1
destp += destspan
if y == 0:
break
y -= 1
return 1
def JM_color_FromSequence(color):
if isinstance(color, (int, float)):
color = color[0]
if not isinstance( color, (list, tuple)):
return -1, []
if len(color) not in (0, 1, 3, 4):
return -1, []
ret = color[:]
for i in range(len(ret)):
if ret[i] < 0 or ret[i] > 1:
ret[i] = 1
return len(ret), ret
def JM_color_count( pm, clip):
rc = dict()
cnt = 0
irect = mupdf.fz_pixmap_bbox( pm)
irect = mupdf.fz_intersect_irect(irect, mupdf.fz_round_rect(JM_rect_from_py(clip)))
stride = pm.stride()
width = irect.x1 - irect.x0
height = irect.y1 - irect.y0
n = pm.n()
substride = width * n
s = stride * (irect.y0 - pm.y()) + (irect.x0 - pm.x()) * n
oldpix = _read_samples( pm, s, n)
cnt = 0
if mupdf.fz_is_empty_irect(irect):
return rc
for i in range( height):
for j in range( 0, substride, n):
newpix = _read_samples( pm, s + j, n)
if newpix != oldpix:
pixel = oldpix
c = rc.get( pixel, None)
if c is not None:
cnt += c
rc[ pixel] = cnt
cnt = 1
oldpix = newpix
else:
cnt += 1
s += stride
pixel = oldpix
c = rc.get( pixel)
if c is not None:
cnt += c
rc[ pixel] = cnt
return rc
def JM_compress_buffer(inbuffer):
'''
compress char* into a new buffer
'''
data, compressed_length = mupdf.fz_new_deflated_data_from_buffer(
inbuffer,
mupdf.FZ_DEFLATE_BEST,
)
if not data or compressed_length == 0:
return None
buf = mupdf.FzBuffer(mupdf.fz_new_buffer_from_data(data, compressed_length))
mupdf.fz_resize_buffer(buf, compressed_length)
return buf
def JM_copy_rectangle(page, area):
need_new_line = 0
buffer = io.StringIO()
for block in page:
if block.m_internal.type != mupdf.FZ_STEXT_BLOCK_TEXT:
continue
for line in block:
line_had_text = 0
for ch in line:
r = JM_char_bbox(line, ch)
if JM_rects_overlap(area, r):
line_had_text = 1
if need_new_line:
buffer.write("\n")
need_new_line = 0
buffer.write(make_escape(ch.m_internal.c))
if line_had_text:
need_new_line = 1
s = buffer.getvalue()
return s
def JM_convert_to_pdf(doc, fp, tp, rotate):
'''
Convert any MuPDF document to a PDF
Returns bytes object containing the PDF, created via 'write' function.
'''
pdfout = mupdf.PdfDocument()
incr = 1
s = fp
e = tp
if fp > tp:
incr = -1
s = tp
e = fp
rot = JM_norm_rotation(rotate)
i = fp
while 1:
if not _INRANGE(i, s, e):
break
page = mupdf.fz_load_page(doc, i)
mediabox = mupdf.fz_bound_page(page)
dev, resources, contents = mupdf.pdf_page_write(pdfout, mediabox)
mupdf.fz_run_page(page, dev, mupdf.FzMatrix(), mupdf.FzCookie())
mupdf.fz_close_device(dev)
dev = None
page_obj = mupdf.pdf_add_page(pdfout, mediabox, rot, resources, contents)
mupdf.pdf_insert_page(pdfout, -1, page_obj)
i += incr
opts = mupdf.PdfWriteOptions()
opts.do_garbage = 4
opts.do_compress = 1
opts.do_compress_images = 1
opts.do_compress_fonts = 1
opts.do_sanitize = 1
opts.do_incremental = 0
opts.do_ascii = 0
opts.do_decompress = 0
opts.do_linear = 0
opts.do_clean = 1
opts.do_pretty = 0
res = mupdf.fz_new_buffer(8192)
out = mupdf.FzOutput(res)
mupdf.pdf_write_document(pdfout, out, opts)
c = mupdf.fz_buffer_extract_copy(res)
assert isinstance(c, bytes)
return c
def JM_create_widget(doc, page, type, fieldname):
old_sigflags = mupdf.pdf_to_int(mupdf.pdf_dict_getp(mupdf.pdf_trailer(doc), "Root/AcroForm/SigFlags"))
annot = mupdf.pdf_create_annot_raw(page, mupdf.PDF_ANNOT_WIDGET)
annot_obj = mupdf.pdf_annot_obj(annot)
try:
JM_set_field_type(doc, annot_obj, type)
mupdf.pdf_dict_put_text_string(annot_obj, PDF_NAME('T'), fieldname)
if type == mupdf.PDF_WIDGET_TYPE_SIGNATURE:
sigflags = old_sigflags | (SigFlag_SignaturesExist | SigFlag_AppendOnly)
mupdf.pdf_dict_putl(
mupdf.pdf_trailer(doc),
mupdf.pdf_new_nt(sigflags),
PDF_NAME('Root'),
PDF_NAME('AcroForm'),
PDF_NAME('SigFlags'),
)
form = mupdf.pdf_dict_getp(mupdf.pdf_trailer(doc), "Root/AcroForm/Fields")
if not form.m_internal:
form = mupdf.pdf_new_array(doc, 1)
mupdf.pdf_dict_putl(
mupdf.pdf_trailer(doc),
form,
PDF_NAME('Root'),
PDF_NAME('AcroForm'),
PDF_NAME('Fields'),
)
mupdf.pdf_array_push(form, annot_obj)
except Exception:
if g_exceptions_verbose: exception_info()
mupdf.pdf_delete_annot(page, annot)
if type == mupdf.PDF_WIDGET_TYPE_SIGNATURE:
mupdf.pdf_dict_putl(
mupdf.pdf_trailer(doc),
mupdf.pdf_new_int(old_sigflags),
PDF_NAME('Root'),
PDF_NAME('AcroForm'),
PDF_NAME('SigFlags'),
)
raise
return annot
def JM_cropbox(page_obj):
'''
return a PDF page's CropBox
'''
if g_use_extra:
return extra.JM_cropbox(page_obj)
mediabox = JM_mediabox(page_obj)
cropbox = mupdf.pdf_to_rect(
mupdf.pdf_dict_get_inheritable(page_obj, PDF_NAME('CropBox'))
)
if mupdf.fz_is_infinite_rect(cropbox) or mupdf.fz_is_empty_rect(cropbox):
cropbox = mediabox
y0 = mediabox.y1 - cropbox.y1
y1 = mediabox.y1 - cropbox.y0
cropbox.y0 = y0
cropbox.y1 = y1
return cropbox
def JM_cropbox_size(page_obj):
rect = JM_cropbox(page_obj)
w = abs(rect.x1 - rect.x0)
h = abs(rect.y1 - rect.y0)
size = mupdf.fz_make_point(w, h)
return size
def JM_derotate_page_matrix(page):
'''
just the inverse of rotation
'''
mp = JM_rotate_page_matrix(page)
return mupdf.fz_invert_matrix(mp)
def JM_embed_file(
pdf,
buf,
filename,
ufilename,
desc,
compress,
):
'''
embed a new file in a PDF (not only /EmbeddedFiles entries)
'''
len_ = 0
val = mupdf.pdf_new_dict(pdf, 6)
mupdf.pdf_dict_put_dict(val, PDF_NAME('CI'), 4)
ef = mupdf.pdf_dict_put_dict(val, PDF_NAME('EF'), 4)
mupdf.pdf_dict_put_text_string(val, PDF_NAME('F'), filename)
mupdf.pdf_dict_put_text_string(val, PDF_NAME('UF'), ufilename)
mupdf.pdf_dict_put_text_string(val, PDF_NAME('Desc'), desc)
mupdf.pdf_dict_put(val, PDF_NAME('Type'), PDF_NAME('Filespec'))
bs = b' '
f = mupdf.pdf_add_stream(
pdf,
mupdf.fz_new_buffer_from_copied_data(bs),
mupdf.PdfObj(),
0,
)
mupdf.pdf_dict_put(ef, PDF_NAME('F'), f)
JM_update_stream(pdf, f, buf, compress)
len_, _ = mupdf.fz_buffer_storage(buf)
mupdf.pdf_dict_put_int(f, PDF_NAME('DL'), len_)
mupdf.pdf_dict_put_int(f, PDF_NAME('Length'), len_)
params = mupdf.pdf_dict_put_dict(f, PDF_NAME('Params'), 4)
mupdf.pdf_dict_put_int(params, PDF_NAME('Size'), len_)
return val
def JM_embedded_clean(pdf):
'''
perform some cleaning if we have /EmbeddedFiles:
(1) remove any /Limits if /Names exists
(2) remove any empty /Collection
(3) set /PageMode/UseAttachments
'''
root = mupdf.pdf_dict_get( mupdf.pdf_trailer( pdf), PDF_NAME('Root'))
coll = mupdf.pdf_dict_get(root, PDF_NAME('Collection'))
if coll.m_internal and mupdf.pdf_dict_len(coll) == 0:
mupdf.pdf_dict_del(root, PDF_NAME('Collection'))
efiles = mupdf.pdf_dict_getl(
root,
PDF_NAME('Names'),
PDF_NAME('EmbeddedFiles'),
PDF_NAME('Names'),
)
if efiles.m_internal:
mupdf.pdf_dict_put_name(root, PDF_NAME('PageMode'), "UseAttachments")
def JM_EscapeStrFromBuffer(buff):
if not buff.m_internal:
return ''
s = mupdf.fz_buffer_extract_copy(buff)
val = PyUnicode_DecodeRawUnicodeEscape(s, errors='replace')
return val
def JM_ensure_identity(pdf):
'''
Store ID in PDF trailer
'''
id_ = mupdf.pdf_dict_get( mupdf.pdf_trailer(pdf), PDF_NAME('ID'))
if not id_.m_internal:
rnd0 = mupdf.fz_memrnd2(16)
rnd = ''
for i in rnd0:
rnd += chr(i)
id_ = mupdf.pdf_dict_put_array( mupdf.pdf_trailer( pdf), PDF_NAME('ID'), 2)
mupdf.pdf_array_push( id_, mupdf.pdf_new_string( rnd, len(rnd)))
mupdf.pdf_array_push( id_, mupdf.pdf_new_string( rnd, len(rnd)))
def JM_ensure_ocproperties(pdf):
'''
Ensure OCProperties, return /OCProperties key
'''
ocp = mupdf.pdf_dict_get(mupdf.pdf_dict_get(mupdf.pdf_trailer(pdf), PDF_NAME('Root')), PDF_NAME('OCProperties'))
if ocp.m_internal:
return ocp
root = mupdf.pdf_dict_get(mupdf.pdf_trailer(pdf), PDF_NAME('Root'))
ocp = mupdf.pdf_dict_put_dict(root, PDF_NAME('OCProperties'), 2)
mupdf.pdf_dict_put_array(ocp, PDF_NAME('OCGs'), 0)
D = mupdf.pdf_dict_put_dict(ocp, PDF_NAME('D'), 5)
mupdf.pdf_dict_put_array(D, PDF_NAME('ON'), 0)
mupdf.pdf_dict_put_array(D, PDF_NAME('OFF'), 0)
mupdf.pdf_dict_put_array(D, PDF_NAME('Order'), 0)
mupdf.pdf_dict_put_array(D, PDF_NAME('RBGroups'), 0)
return ocp
def JM_expand_fname(name):
'''
Make /DA string of annotation
'''
if not name: return "Helv"
if name.startswith("Co"): return "Cour"
if name.startswith("co"): return "Cour"
if name.startswith("Ti"): return "TiRo"
if name.startswith("ti"): return "TiRo"
if name.startswith("Sy"): return "Symb"
if name.startswith("sy"): return "Symb"
if name.startswith("Za"): return "ZaDb"
if name.startswith("za"): return "ZaDb"
return "Helv"
def JM_field_type_text(wtype):
'''
String from widget type
'''
if wtype == mupdf.PDF_WIDGET_TYPE_BUTTON:
return "Button"
if wtype == mupdf.PDF_WIDGET_TYPE_CHECKBOX:
return "CheckBox"
if wtype == mupdf.PDF_WIDGET_TYPE_RADIOBUTTON:
return "RadioButton"
if wtype == mupdf.PDF_WIDGET_TYPE_TEXT:
return "Text"
if wtype == mupdf.PDF_WIDGET_TYPE_LISTBOX:
return "ListBox"
if wtype == mupdf.PDF_WIDGET_TYPE_COMBOBOX:
return "ComboBox"
if wtype == mupdf.PDF_WIDGET_TYPE_SIGNATURE:
return "Signature"
return "unknown"
def JM_fill_pixmap_rect_with_color(dest, col, b):
assert isinstance(dest, mupdf.FzPixmap)
b = mupdf.fz_intersect_irect(b, mupdf.fz_pixmap_bbox( dest))
w = b.x1 - b.x0
y = b.y1 - b.y0
if w <= 0 or y <= 0:
return 0
destspan = dest.stride()
destp = destspan * (b.y0 - dest.y()) + dest.n() * (b.x0 - dest.x())
while 1:
s = destp
for x in range(w):
for i in range( dest.n()):
mupdf.fz_samples_set(dest, s, col[i])
s += 1
destp += destspan
y -= 1
if y == 0:
break
return 1
def JM_find_annot_irt(annot):
'''
Return the first annotation whose /IRT key ("In Response To") points to
annot. Used to remove the response chain of a given annotation.
'''
assert isinstance(annot, mupdf.PdfAnnot)
irt_annot = None
annot_obj = mupdf.pdf_annot_obj(annot)
found = 0
page = mupdf.pdf_annot_page(annot)
irt_annot = mupdf.pdf_first_annot(page)
while 1:
assert isinstance(irt_annot, mupdf.PdfAnnot)
if not irt_annot.m_internal:
break
irt_annot_obj = mupdf.pdf_annot_obj(irt_annot)
o = mupdf.pdf_dict_gets(irt_annot_obj, 'IRT')
if o.m_internal:
if not mupdf.pdf_objcmp(o, annot_obj):
found = 1
break
irt_annot = mupdf.pdf_next_annot(irt_annot)
if found:
return irt_annot
def JM_font_ascender(font):
'''
need own versions of ascender / descender
'''
assert isinstance(font, mupdf.FzFont)
if g_skip_quad_corrections:
return 0.8
return mupdf.fz_font_ascender(font)
def JM_font_descender(font):
'''
need own versions of ascender / descender
'''
assert isinstance(font, mupdf.FzFont)
if g_skip_quad_corrections:
return -0.2
ret = mupdf.fz_font_descender(font)
return ret
def JM_is_word_delimiter(ch, delimiters):
"""Check if ch is an extra word delimiting character.
"""
if ch <= 32 or ch == 160:
return True
if not delimiters:
return False
char = chr(ch)
for d in delimiters:
if d == char:
return True
return False
def JM_font_name(font):
assert isinstance(font, mupdf.FzFont)
name = mupdf.fz_font_name(font)
s = name.find('+')
if g_subset_fontnames or s == -1 or s != 6:
return name
return name[s + 1:]
def JM_gather_fonts(pdf, dict_, fontlist, stream_xref):
rc = 1
n = mupdf.pdf_dict_len(dict_)
for i in range(n):
refname = mupdf.pdf_dict_get_key(dict_, i)
fontdict = mupdf.pdf_dict_get_val(dict_, i)
if not mupdf.pdf_is_dict(fontdict):
mupdf.fz_warn( f"'{mupdf.pdf_to_name(refname)}' is no font dict ({mupdf.pdf_to_num(fontdict)} 0 R)")
continue
subtype = mupdf.pdf_dict_get(fontdict, mupdf.PDF_ENUM_NAME_Subtype)
basefont = mupdf.pdf_dict_get(fontdict, mupdf.PDF_ENUM_NAME_BaseFont)
if not basefont.m_internal or mupdf.pdf_is_null(basefont):
name = mupdf.pdf_dict_get(fontdict, mupdf.PDF_ENUM_NAME_Name)
else:
name = basefont
encoding = mupdf.pdf_dict_get(fontdict, mupdf.PDF_ENUM_NAME_Encoding)
if mupdf.pdf_is_dict(encoding):
encoding = mupdf.pdf_dict_get(encoding, mupdf.PDF_ENUM_NAME_BaseEncoding)
xref = mupdf.pdf_to_num(fontdict)
ext = "n/a"
if xref:
ext = JM_get_fontextension(pdf, xref)
entry = (
xref,
ext,
mupdf.pdf_to_name(subtype),
JM_EscapeStrFromStr(mupdf.pdf_to_name(name)),
mupdf.pdf_to_name(refname),
mupdf.pdf_to_name(encoding),
stream_xref,
)
fontlist.append(entry)
return rc
def JM_gather_forms(doc, dict_: mupdf.PdfObj, imagelist, stream_xref: int):
'''
Store info of a /Form xobject in Python list
'''
assert isinstance(doc, mupdf.PdfDocument)
rc = 1
n = mupdf.pdf_dict_len(dict_)
for i in range(n):
refname = mupdf.pdf_dict_get_key( dict_, i)
imagedict = mupdf.pdf_dict_get_val(dict_, i)
if not mupdf.pdf_is_dict(imagedict):
mupdf.fz_warn( f"'{mupdf.pdf_to_name(refname)}' is no form dict ({mupdf.pdf_to_num(imagedict)} 0 R)")
continue
type_ = mupdf.pdf_dict_get(imagedict, PDF_NAME('Subtype'))
if not mupdf.pdf_name_eq(type_, PDF_NAME('Form')):
continue
o = mupdf.pdf_dict_get(imagedict, PDF_NAME('BBox'))
m = mupdf.pdf_dict_get(imagedict, PDF_NAME('Matrix'))
if m.m_internal:
mat = mupdf.pdf_to_matrix(m)
else:
mat = mupdf.FzMatrix()
if o.m_internal:
bbox = mupdf.fz_transform_rect( mupdf.pdf_to_rect(o), mat)
else:
bbox = mupdf.FzRect(mupdf.FzRect.Fixed_INFINITE)
xref = mupdf.pdf_to_num(imagedict)
entry = (
xref,
mupdf.pdf_to_name( refname),
stream_xref,
JM_py_from_rect(bbox),
)
imagelist.append(entry)
return rc
def JM_gather_images(doc: mupdf.PdfDocument, dict_: mupdf.PdfObj, imagelist, stream_xref: int):
'''
Store info of an image in Python list
'''
rc = 1
n = mupdf.pdf_dict_len( dict_)
for i in range(n):
refname = mupdf.pdf_dict_get_key(dict_, i)
imagedict = mupdf.pdf_dict_get_val(dict_, i)
if not mupdf.pdf_is_dict(imagedict):
mupdf.fz_warn(f"'{mupdf.pdf_to_name(refname)}' is no image dict ({mupdf.pdf_to_num(imagedict)} 0 R)")
continue
type_ = mupdf.pdf_dict_get(imagedict, PDF_NAME('Subtype'))
if not mupdf.pdf_name_eq(type_, PDF_NAME('Image')):
continue
xref = mupdf.pdf_to_num(imagedict)
gen = 0
smask = mupdf.pdf_dict_geta(imagedict, PDF_NAME('SMask'), PDF_NAME('Mask'))
if smask.m_internal:
gen = mupdf.pdf_to_num(smask)
filter_ = mupdf.pdf_dict_geta(imagedict, PDF_NAME('Filter'), PDF_NAME('F'))
if mupdf.pdf_is_array(filter_):
filter_ = mupdf.pdf_array_get(filter_, 0)
altcs = mupdf.PdfObj(0)
cs = mupdf.pdf_dict_geta(imagedict, PDF_NAME('ColorSpace'), PDF_NAME('CS'))
if mupdf.pdf_is_array(cs):
cses = cs
cs = mupdf.pdf_array_get(cses, 0)
if (mupdf.pdf_name_eq(cs, PDF_NAME('DeviceN'))
or mupdf.pdf_name_eq(cs, PDF_NAME('Separation'))
):
altcs = mupdf.pdf_array_get(cses, 2)
if mupdf.pdf_is_array(altcs):
altcs = mupdf.pdf_array_get(altcs, 0)
width = mupdf.pdf_dict_geta(imagedict, PDF_NAME('Width'), PDF_NAME('W'))
height = mupdf.pdf_dict_geta(imagedict, PDF_NAME('Height'), PDF_NAME('H'))
bpc = mupdf.pdf_dict_geta(imagedict, PDF_NAME('BitsPerComponent'), PDF_NAME('BPC'))
entry = (
xref,
gen,
mupdf.pdf_to_int(width),
mupdf.pdf_to_int(height),
mupdf.pdf_to_int(bpc),
JM_EscapeStrFromStr(mupdf.pdf_to_name(cs)),
JM_EscapeStrFromStr(mupdf.pdf_to_name(altcs)),
JM_EscapeStrFromStr(mupdf.pdf_to_name(refname)),
JM_EscapeStrFromStr(mupdf.pdf_to_name(filter_)),
stream_xref,
)
imagelist.append(entry)
return rc
def JM_get_annot_by_xref(page, xref):
'''
retrieve annot by its xref
'''
assert isinstance(page, mupdf.PdfPage)
found = 0
annot = mupdf.pdf_first_annot(page)
while 1:
if not annot.m_internal:
break
if xref == mupdf.pdf_to_num(mupdf.pdf_annot_obj(annot)):
found = 1
break
annot = mupdf.pdf_next_annot( annot)
if not found:
raise Exception("xref %d is not an annot of this page" % xref)
return annot
def JM_get_annot_by_name(page, name):
'''
retrieve annot by name (/NM key)
'''
assert isinstance(page, mupdf.PdfPage)
if not name:
return
found = 0
annot = mupdf.pdf_first_annot(page)
while 1:
if not annot.m_internal:
break
response, len_ = mupdf.pdf_to_string(mupdf.pdf_dict_gets(mupdf.pdf_annot_obj(annot), "NM"))
if name == response:
found = 1
break
annot = mupdf.pdf_next_annot(annot)
if not found:
raise Exception("'%s' is not an annot of this page" % name)
return annot
def JM_get_annot_id_list(page):
names = []
annots = mupdf.pdf_dict_get( page.obj(), mupdf.PDF_ENUM_NAME_Annots)
if not annots.m_internal:
return names
for i in range( mupdf.pdf_array_len(annots)):
annot_obj = mupdf.pdf_array_get(annots, i)
name = mupdf.pdf_dict_gets(annot_obj, "NM")
if name.m_internal:
names.append(
mupdf.pdf_to_text_string(name)
)
return names
def JM_get_annot_xref_list( page_obj):
'''
return the xrefs and /NM ids of a page's annots, links and fields
'''
if g_use_extra:
names = extra.JM_get_annot_xref_list( page_obj)
return names
names = []
annots = mupdf.pdf_dict_get( page_obj, PDF_NAME('Annots'))
n = mupdf.pdf_array_len( annots)
for i in range( n):
annot_obj = mupdf.pdf_array_get( annots, i)
xref = mupdf.pdf_to_num( annot_obj)
subtype = mupdf.pdf_dict_get( annot_obj, PDF_NAME('Subtype'))
if not subtype.m_internal:
continue
type_ = mupdf.pdf_annot_type_from_string( mupdf.pdf_to_name( subtype))
if type_ == mupdf.PDF_ANNOT_UNKNOWN:
continue
id_ = mupdf.pdf_dict_gets( annot_obj, "NM")
names.append( (xref, type_, mupdf.pdf_to_text_string( id_)))
return names
def JM_get_border_style(style):
'''
return pdf_obj "border style" from Python str
'''
val = mupdf.PDF_ENUM_NAME_S
if style is None:
return val
s = style
if s.startswith("b") or s.startswith("B"): val = mupdf.PDF_ENUM_NAME_B
elif s.startswith("d") or s.startswith("D"): val = mupdf.PDF_ENUM_NAME_D
elif s.startswith("i") or s.startswith("I"): val = mupdf.PDF_ENUM_NAME_I
elif s.startswith("u") or s.startswith("U"): val = mupdf.PDF_ENUM_NAME_U
elif s.startswith("s") or s.startswith("S"): val = mupdf.PDF_ENUM_NAME_S
return val
def JM_get_font(
fontname,
fontfile,
fontbuffer,
script,
lang,
ordering,
is_bold,
is_italic,
is_serif,
embed,
):
'''
return a fz_font from a number of parameters
'''
def fertig(font):
if not font.m_internal:
raise RuntimeError(MSG_FONT_FAILED)
if not font.m_internal.flags.never_embed:
mupdf.fz_set_font_embedding(font, embed)
return font
index = 0
font = None
if fontfile:
font = mupdf.fz_new_font_from_file( None, fontfile, index, 0)
return fertig(font)
if fontbuffer:
res = JM_BufferFromBytes(fontbuffer)
font = mupdf.fz_new_font_from_buffer( None, res, index, 0)
return fertig(font)
if ordering > -1:
font = mupdf.fz_new_cjk_font(ordering)
return fertig(font)
if fontname:
font = mupdf.fz_new_base14_font(fontname)
if font.m_internal:
return fertig(font)
font = mupdf.fz_new_builtin_font(fontname, is_bold, is_italic)
return fertig(font)
data, size, index = mupdf.fz_lookup_noto_font( script, lang)
font = None
if data:
font = mupdf.fz_new_font_from_memory( None, data, size, index, 0)
if font.m_internal:
return fertig(font)
font = mupdf.fz_load_fallback_font( script, lang, is_serif, is_bold, is_italic)
return fertig(font)
def JM_get_fontbuffer(doc, xref):
'''
Return the contents of a font file, identified by xref
'''
if xref < 1:
return
o = mupdf.pdf_load_object(doc, xref)
desft = mupdf.pdf_dict_get(o, PDF_NAME('DescendantFonts'))
if desft.m_internal:
obj = mupdf.pdf_resolve_indirect(mupdf.pdf_array_get(desft, 0))
obj = mupdf.pdf_dict_get(obj, PDF_NAME('FontDescriptor'))
else:
obj = mupdf.pdf_dict_get(o, PDF_NAME('FontDescriptor'))
if not obj.m_internal:
print(f"invalid font - FontDescriptor missing")
return
o = obj
stream = None
obj = mupdf.pdf_dict_get(o, PDF_NAME('FontFile'))
if obj.m_internal:
stream = obj
obj = mupdf.pdf_dict_get(o, PDF_NAME('FontFile2'))
if obj.m_internal:
stream = obj
obj = mupdf.pdf_dict_get(o, PDF_NAME('FontFile3'))
if obj.m_internal:
stream = obj
obj = mupdf.pdf_dict_get(obj, PDF_NAME('Subtype'))
if obj.m_internal and not mupdf.pdf_is_name(obj):
print("invalid font descriptor subtype")
return
if mupdf.pdf_name_eq(obj, PDF_NAME('Type1C')):
pass
elif mupdf.pdf_name_eq(obj, PDF_NAME('CIDFontType0C')):
pass
elif mupdf.pdf_name_eq(obj, PDF_NAME('OpenType')):
pass
else:
print('warning: unhandled font type {pdf_to_name(ctx, obj)!r}')
if not stream:
print('warning: unhandled font type')
return
return mupdf.pdf_load_stream(stream)
def JM_get_resource_properties(ref):
'''
Return the items of Resources/Properties (used for Marked Content)
Argument may be e.g. a page object or a Form XObject
'''
properties = mupdf.pdf_dict_getl(ref, PDF_NAME('Resources'), PDF_NAME('Properties'))
if not properties.m_internal:
return ()
else:
n = mupdf.pdf_dict_len(properties)
if n < 1:
return ()
rc = []
for i in range(n):
key = mupdf.pdf_dict_get_key(properties, i)
val = mupdf.pdf_dict_get_val(properties, i)
c = mupdf.pdf_to_name(key)
xref = mupdf.pdf_to_num(val)
rc.append((c, xref))
return rc
def JM_get_widget_by_xref( page, xref):
'''
retrieve widget by its xref
'''
found = False
annot = mupdf.pdf_first_widget( page)
while annot.m_internal:
annot_obj = mupdf.pdf_annot_obj( annot)
if xref == mupdf.pdf_to_num( annot_obj):
found = True
break
annot = mupdf.pdf_next_widget( annot)
if not found:
raise Exception( f"xref {xref} is not a widget of this page")
return Annot( annot)
def JM_get_widget_properties(annot, Widget):
'''
Populate a Python Widget object with the values from a PDF form field.
Called by "Page.first_widget" and "Widget.next".
'''
annot_obj = mupdf.pdf_annot_obj(annot.this)
page = mupdf.pdf_annot_page(annot.this)
pdf = page.doc()
tw = annot
def SETATTR(key, value):
setattr(Widget, key, value)
def SETATTR_DROP(mod, key, value):
setattr(mod, key, value)
field_type = mupdf.pdf_widget_type(tw.this)
Widget.field_type = field_type
if field_type == mupdf.PDF_WIDGET_TYPE_SIGNATURE:
if mupdf.pdf_signature_is_signed(pdf, annot_obj):
SETATTR("is_signed", True)
else:
SETATTR("is_signed",False)
else:
SETATTR("is_signed", None)
SETATTR_DROP(Widget, "border_style", JM_UnicodeFromStr(mupdf.pdf_field_border_style(annot_obj)))
SETATTR_DROP(Widget, "field_type_string", JM_UnicodeFromStr(JM_field_type_text(field_type)))
field_name = mupdf.pdf_load_field_name(annot_obj)
SETATTR_DROP(Widget, "field_name", field_name)
obj = mupdf.pdf_dict_get(annot_obj, PDF_NAME('TU'))
if obj.m_internal:
label = mupdf.pdf_to_text_string(obj)
SETATTR_DROP(Widget, "field_label", label)
fvalue = None
if field_type == mupdf.PDF_WIDGET_TYPE_RADIOBUTTON:
obj = mupdf.pdf_dict_get( annot_obj, PDF_NAME('Parent'))
if obj.m_internal:
SETATTR_DROP(Widget, "rb_parent", mupdf.pdf_to_num( obj))
obj = mupdf.pdf_dict_get(annot_obj, PDF_NAME('AS'))
if obj.m_internal:
fvalue = mupdf.pdf_to_name(obj)
if not fvalue:
fvalue = mupdf.pdf_field_value(annot_obj)
SETATTR_DROP(Widget, "field_value", JM_UnicodeFromStr(fvalue))
SETATTR_DROP(Widget, "field_display", mupdf.pdf_field_display(annot_obj))
border_width = mupdf.pdf_to_real(mupdf.pdf_dict_getl(annot_obj, PDF_NAME('BS'), PDF_NAME('W')))
if border_width == 0:
border_width = 1
SETATTR_DROP(Widget, "border_width", border_width)
obj = mupdf.pdf_dict_getl(annot_obj, PDF_NAME('BS'), PDF_NAME('D'))
if mupdf.pdf_is_array(obj):
n = mupdf.pdf_array_len(obj)
d = [0] * n
for i in range(n):
d[i] = mupdf.pdf_to_int(mupdf.pdf_array_get(obj, i))
SETATTR_DROP(Widget, "border_dashes", d)
SETATTR_DROP(Widget, "text_maxlen", mupdf.pdf_text_widget_max_len(tw.this))
SETATTR_DROP(Widget, "text_format", mupdf.pdf_text_widget_format(tw.this))
obj = mupdf.pdf_dict_getl(annot_obj, PDF_NAME('MK'), PDF_NAME('BG'))
if mupdf.pdf_is_array(obj):
n = mupdf.pdf_array_len(obj)
col = [0] * n
for i in range(n):
col[i] = mupdf.pdf_to_real(mupdf.pdf_array_get(obj, i))
SETATTR_DROP(Widget, "fill_color", col)
obj = mupdf.pdf_dict_getl(annot_obj, PDF_NAME('MK'), PDF_NAME('BC'))
if mupdf.pdf_is_array(obj):
n = mupdf.pdf_array_len(obj)
col = [0] * n
for i in range(n):
col[i] = mupdf.pdf_to_real(mupdf.pdf_array_get(obj, i))
SETATTR_DROP(Widget, "border_color", col)
SETATTR_DROP(Widget, "choice_values", JM_choice_options(annot))
da = mupdf.pdf_to_text_string(mupdf.pdf_dict_get_inheritable(annot_obj, PDF_NAME('DA')))
SETATTR_DROP(Widget, "_text_da", JM_UnicodeFromStr(da))
obj = mupdf.pdf_dict_getl(annot_obj, PDF_NAME('MK'), PDF_NAME('CA'))
if obj.m_internal:
SETATTR_DROP(Widget, "button_caption", JM_UnicodeFromStr(mupdf.pdf_to_text_string(obj)))
SETATTR_DROP(Widget, "field_flags", mupdf.pdf_field_flags(annot_obj))
Widget._parse_da()
s = mupdf.pdf_dict_get(annot_obj, PDF_NAME('A'))
ss = JM_get_script(s)
SETATTR_DROP(Widget, "script", ss)
SETATTR_DROP(Widget, "script_stroke",
JM_get_script(mupdf.pdf_dict_getl(annot_obj, PDF_NAME('AA'), PDF_NAME('K')))
)
SETATTR_DROP(Widget, "script_format",
JM_get_script(mupdf.pdf_dict_getl(annot_obj, PDF_NAME('AA'), PDF_NAME('F')))
)
SETATTR_DROP(Widget, "script_change",
JM_get_script(mupdf.pdf_dict_getl(annot_obj, PDF_NAME('AA'), PDF_NAME('V')))
)
SETATTR_DROP(Widget, "script_calc",
JM_get_script(mupdf.pdf_dict_getl(annot_obj, PDF_NAME('AA'), PDF_NAME('C')))
)
SETATTR_DROP(Widget, "script_blur",
JM_get_script(mupdf.pdf_dict_getl(annot_obj, PDF_NAME('AA'), mupdf.pdf_new_name('Bl')))
)
SETATTR_DROP(Widget, "script_focus",
JM_get_script(mupdf.pdf_dict_getl(annot_obj, PDF_NAME('AA'), mupdf.pdf_new_name('Fo')))
)
def JM_get_fontextension(doc, xref):
'''
Return the file extension of a font file, identified by xref
'''
if xref < 1:
return "n/a"
o = mupdf.pdf_load_object(doc, xref)
desft = mupdf.pdf_dict_get(o, PDF_NAME('DescendantFonts'))
if desft.m_internal:
obj = mupdf.pdf_resolve_indirect(mupdf.pdf_array_get(desft, 0))
obj = mupdf.pdf_dict_get(obj, PDF_NAME('FontDescriptor'))
else:
obj = mupdf.pdf_dict_get(o, PDF_NAME('FontDescriptor'))
if not obj.m_internal:
return "n/a"
o = obj
obj = mupdf.pdf_dict_get(o, PDF_NAME('FontFile'))
if obj.m_internal:
return "pfa"
obj = mupdf.pdf_dict_get(o, PDF_NAME('FontFile2'))
if obj.m_internal:
return "ttf"
obj = mupdf.pdf_dict_get(o, PDF_NAME('FontFile3'))
if obj.m_internal:
obj = mupdf.pdf_dict_get(obj, PDF_NAME('Subtype'))
if obj.m_internal and not mupdf.pdf_is_name(obj):
PySys_WriteStdout("invalid font descriptor subtype")
return "n/a"
if mupdf.pdf_name_eq(obj, PDF_NAME('Type1C')):
return "cff"
elif mupdf.pdf_name_eq(obj, PDF_NAME('CIDFontType0C')):
return "cid"
elif mupdf.pdf_name_eq(obj, PDF_NAME('OpenType')):
return "otf"
else:
PySys_WriteStdout("unhandled font type '%s'", mupdf.pdf_to_name(obj))
return "n/a"
def JM_get_ocg_arrays_imp(arr):
'''
Get OCG arrays from OC configuration
Returns dict {"basestate":name, "on":list, "off":list, "rbg":list, "locked":list}
'''
list_ = list()
if mupdf.pdf_is_array( arr):
n = mupdf.pdf_array_len( arr)
for i in range(n):
obj = mupdf.pdf_array_get( arr, i)
item = mupdf.pdf_to_num( obj)
if item not in list_:
list_.append(item)
return list_
def JM_get_ocg_arrays(conf):
rc = dict()
arr = mupdf.pdf_dict_get( conf, PDF_NAME('ON'))
list_ = JM_get_ocg_arrays_imp( arr)
if list_:
rc["on"] = list_
arr = mupdf.pdf_dict_get( conf, PDF_NAME('OFF'))
list_ = JM_get_ocg_arrays_imp( arr)
if list_:
rc["off"] = list_
arr = mupdf.pdf_dict_get( conf, PDF_NAME('Locked'))
list_ = JM_get_ocg_arrays_imp( arr)
if list_:
rc['locked'] = list_
list_ = list()
arr = mupdf.pdf_dict_get( conf, PDF_NAME('RBGroups'))
if mupdf.pdf_is_array( arr):
n = mupdf.pdf_array_len( arr)
for i in range(n):
obj = mupdf.pdf_array_get( arr, i)
list1 = JM_get_ocg_arrays_imp( obj)
list_.append(list1)
if list_:
rc["rbgroups"] = list_
obj = mupdf.pdf_dict_get( conf, PDF_NAME('BaseState'))
if obj.m_internal:
state = mupdf.pdf_to_name( obj)
rc["basestate"] = state
return rc
def JM_get_page_labels(liste, nums):
n = mupdf.pdf_array_len(nums)
for i in range(0, n, 2):
key = mupdf.pdf_resolve_indirect( mupdf.pdf_array_get(nums, i))
pno = mupdf.pdf_to_int(key)
val = mupdf.pdf_resolve_indirect( mupdf.pdf_array_get(nums, i + 1))
res = JM_object_to_buffer(val, 1, 0)
c = mupdf.fz_buffer_extract(res)
assert isinstance(c, bytes)
c = c.decode('utf-8')
liste.append( (pno, c))
def JM_get_script(key):
'''
JavaScript extractor
Returns either the script source or None. Parameter is a PDF action
dictionary, which must have keys /S and /JS. The value of /S must be
'/JavaScript'. The value of /JS is returned.
'''
if not key.m_internal:
return
j = mupdf.pdf_dict_get(key, PDF_NAME('S'))
jj = mupdf.pdf_to_name(j)
if jj == "JavaScript":
js = mupdf.pdf_dict_get(key, PDF_NAME('JS'))
if not js.m_internal:
return
else:
return
if mupdf.pdf_is_string(js):
script = JM_UnicodeFromStr(mupdf.pdf_to_text_string(js))
elif mupdf.pdf_is_stream(js):
res = mupdf.pdf_load_stream(js)
script = JM_EscapeStrFromBuffer(res)
else:
return
if script:
return script
return
def JM_have_operation(pdf):
'''
Ensure valid journalling state
'''
if pdf.m_internal.journal and not mupdf.pdf_undoredo_step(pdf, 0):
return 0
return 1
def JM_image_extension(type_):
'''
return extension for fitz image type
'''
if type_ == mupdf.FZ_IMAGE_FAX: return "fax"
if type_ == mupdf.FZ_IMAGE_RAW: return "raw"
if type_ == mupdf.FZ_IMAGE_FLATE: return "flate"
if type_ == mupdf.FZ_IMAGE_LZW: return "lzw"
if type_ == mupdf.FZ_IMAGE_RLD: return "rld"
if type_ == mupdf.FZ_IMAGE_BMP: return "bmp"
if type_ == mupdf.FZ_IMAGE_GIF: return "gif"
if type_ == mupdf.FZ_IMAGE_JBIG2: return "jb2"
if type_ == mupdf.FZ_IMAGE_JPEG: return "jpeg"
if type_ == mupdf.FZ_IMAGE_JPX: return "jpx"
if type_ == mupdf.FZ_IMAGE_JXR: return "jxr"
if type_ == mupdf.FZ_IMAGE_PNG: return "png"
if type_ == mupdf.FZ_IMAGE_PNM: return "pnm"
if type_ == mupdf.FZ_IMAGE_TIFF: return "tiff"
return "n/a"
g_img_info = None
def JM_image_filter(opaque, ctm, name, image):
assert isinstance(ctm, mupdf.FzMatrix)
r = mupdf.FzRect(mupdf.FzRect.Fixed_UNIT)
q = mupdf.fz_transform_quad( mupdf.fz_quad_from_rect(r), ctm)
if mupdf_version_tuple >= (1, 22):
q = mupdf.fz_transform_quad( q, g_img_info_matrix)
temp = name, JM_py_from_quad(q)
g_img_info.append(temp)
def JM_image_profile( imagedata, keep_image):
'''
Return basic properties of an image provided as bytes or bytearray
The function creates an fz_image and optionally returns it.
'''
if not imagedata:
return None
len_ = len( imagedata)
if len_ < 8:
sys.stderr.write( "bad image data\n")
return None
c = imagedata
type_ = mupdf.fz_recognize_image_format( c)
if type_ == mupdf.FZ_IMAGE_UNKNOWN:
return None
if keep_image:
res = mupdf.fz_new_buffer_from_copied_data( c, len_)
else:
res = mupdf.fz_new_buffer_from_shared_data( c, len_)
image = mupdf.fz_new_image_from_buffer( res)
ctm = mupdf.fz_image_orientation_matrix( image)
xres, yres = mupdf.fz_image_resolution(image)
orientation = mupdf.fz_image_orientation( image)
cs_name = mupdf.fz_colorspace_name( image.colorspace())
result = dict()
result[ dictkey_width] = image.w()
result[ dictkey_height] = image.h()
result[ "orientation"] = orientation
result[ dictkey_matrix] = JM_py_from_matrix(ctm)
result[ dictkey_xres] = xres
result[ dictkey_yres] = yres
result[ dictkey_colorspace] = image.n()
result[ dictkey_bpc] = image.bpc()
result[ dictkey_ext] = JM_image_extension(type_)
result[ dictkey_cs_name] = cs_name
if keep_image:
result[ dictkey_image] = image
return result
if mupdf_version_tuple >= (1, 22):
def JM_image_reporter(page):
doc = page.doc()
global g_img_info_matrix
g_img_info_matrix = mupdf.FzMatrix()
mediabox = mupdf.FzRect()
mupdf.pdf_page_transform(page, mediabox, g_img_info_matrix)
class SanitizeFilterOptions(mupdf.PdfSanitizeFilterOptions2):
def __init__(self):
super().__init__()
self.use_virtual_image_filter()
def image_filter(self, ctx, ctm, name, image):
JM_image_filter(None, mupdf.FzMatrix(ctm), name, image)
sanitize_filter_options = SanitizeFilterOptions()
filter_options = _make_PdfFilterOptions(
instance_forms=1,
ascii=1,
no_update=1,
sanitize=1,
sopts=sanitize_filter_options,
)
global g_img_info
g_img_info = []
mupdf.pdf_filter_page_contents( doc, page, filter_options)
rc = tuple(g_img_info)
g_img_info = []
return rc
else:
def JM_filter_content_stream(
doc,
in_stm,
in_res,
transform,
filter_,
struct_parents,
):
'''
Returns (out_buf, out_res).
'''
out_buf = mupdf.FzBuffer( 1024)
proc_buffer = mupdf.pdf_new_buffer_processor( out_buf, filter_.ascii)
if filter_.sanitize:
out_res = mupdf.pdf_new_dict( doc, 1)
proc_filter = mupdf.pdf_new_filter_processor( doc, proc_buffer, in_res, out_res, struct_parents, transform, filter_)
mupdf.pdf_process_contents( proc_filter, doc, in_res, in_stm, mupdf.FzCookie())
mupdf.pdf_close_processor( proc_filter)
else:
out_res = in_res
mupdf.pdf_process_contents( proc_buffer, doc, in_res, in_stm, mupdf.FzCookie())
mupdf.pdf_close_processor( proc_buffer)
return out_buf, out_res
def JM_image_reporter(page):
doc = page.doc()
filter_ = JM_image_reporter_Filter()
filter_._page = page
filter_.recurse = 0
filter_.instance_forms = 1
filter_.sanitize = 1
filter_.ascii = 1
ctm = mupdf.FzMatrix()
mupdf.pdf_page_transform( page, mupdf.FzRect(0, 0, 0, 0), ctm)
struct_parents_obj = mupdf.pdf_dict_get( page.obj(), PDF_NAME('StructParents'))
struct_parents = -1
if mupdf.pdf_is_number( struct_parents_obj):
struct_parents = mupdf.pdf_to_int( struct_parents_obj)
contents = mupdf.pdf_page_contents( page)
old_res = mupdf.pdf_page_resources( page)
global g_img_info
g_img_info = []
buffer_, new_res = JM_filter_content_stream( doc, contents, old_res, ctm, filter_, struct_parents)
rc = tuple( g_img_info)
return rc
def JM_fitz_config():
have_TOFU = not hasattr(mupdf, 'TOFU')
have_TOFU_BASE14 = not hasattr(mupdf, 'TOFU_BASE14')
have_TOFU_CJK = not hasattr(mupdf, 'TOFU_CJK')
have_TOFU_CJK_EXT = not hasattr(mupdf, 'TOFU_CJK_EXT')
have_TOFU_CJK_LANG = not hasattr(mupdf, 'TOFU_CJK_LANG')
have_TOFU_EMOJI = not hasattr(mupdf, 'TOFU_EMOJI')
have_TOFU_HISTORIC = not hasattr(mupdf, 'TOFU_HISTORIC')
have_TOFU_SIL = not hasattr(mupdf, 'TOFU_SIL')
have_TOFU_SYMBOL = not hasattr(mupdf, 'TOFU_SYMBOL')
ret = dict()
ret["base14"] = have_TOFU_BASE14
ret["cbz"] = bool(mupdf.FZ_ENABLE_CBZ)
ret["epub"] = bool(mupdf.FZ_ENABLE_EPUB)
ret["html"] = bool(mupdf.FZ_ENABLE_HTML)
ret["icc"] = bool(mupdf.FZ_ENABLE_ICC)
ret["img"] = bool(mupdf.FZ_ENABLE_IMG)
ret["jpx"] = bool(mupdf.FZ_ENABLE_JPX)
ret["js"] = bool(mupdf.FZ_ENABLE_JS)
ret["pdf"] = bool(mupdf.FZ_ENABLE_PDF)
ret["plotter-cmyk"] = bool(mupdf.FZ_PLOTTERS_CMYK)
ret["plotter-g"] = bool(mupdf.FZ_PLOTTERS_G)
ret["plotter-n"] = bool(mupdf.FZ_PLOTTERS_N)
ret["plotter-rgb"] = bool(mupdf.FZ_PLOTTERS_RGB)
ret["py-memory"] = bool(JM_MEMORY)
ret["svg"] = bool(mupdf.FZ_ENABLE_SVG)
ret["tofu"] = have_TOFU
ret["tofu-cjk"] = have_TOFU_CJK
ret["tofu-cjk-ext"] = have_TOFU_CJK_EXT
ret["tofu-cjk-lang"] = have_TOFU_CJK_LANG
ret["tofu-emoji"] = have_TOFU_EMOJI
ret["tofu-historic"] = have_TOFU_HISTORIC
ret["tofu-sil"] = have_TOFU_SIL
ret["tofu-symbol"] = have_TOFU_SYMBOL
ret["xps"] = bool(mupdf.FZ_ENABLE_XPS)
return ret
def JM_insert_contents(pdf, pageref, newcont, overlay):
'''
Insert a buffer as a new separate /Contents object of a page.
1. Create a new stream object from buffer 'newcont'
2. If /Contents already is an array, then just prepend or append this object
3. Else, create new array and put old content obj and this object into it.
If the page had no /Contents before, just create a 1-item array.
'''
contents = mupdf.pdf_dict_get(pageref, PDF_NAME('Contents'))
newconts = mupdf.pdf_add_stream(pdf, newcont, mupdf.PdfObj(), 0)
xref = mupdf.pdf_to_num(newconts)
if mupdf.pdf_is_array(contents):
if overlay:
mupdf.pdf_array_push(contents, newconts)
else:
mupdf.pdf_array_insert(contents, newconts, 0)
else:
carr = mupdf.pdf_new_array(pdf, 5)
if overlay:
if contents.m_internal:
mupdf.pdf_array_push(carr, contents)
mupdf.pdf_array_push(carr, newconts)
else:
mupdf.pdf_array_push(carr, newconts)
if contents.m_internal:
mupdf.pdf_array_push(carr, contents)
mupdf.pdf_dict_put(pageref, PDF_NAME('Contents'), carr)
return xref
def JM_insert_font(pdf, bfname, fontfile, fontbuffer, set_simple, idx, wmode, serif, encoding, ordering):
'''
Insert a font in a PDF
'''
font = None
res = None
data = None
ixref = 0
index = 0
simple = 0
value=None
name=None
subt=None
exto = None
ENSURE_OPERATION(pdf)
if ordering > -1:
data, size, index = mupdf.fz_lookup_cjk_font(ordering)
if data:
font = mupdf.fz_new_font_from_memory(None, data, size, index, 0)
font_obj = mupdf.pdf_add_cjk_font(pdf, font, ordering, wmode, serif)
exto = "n/a"
simple = 0
else:
if bfname:
data, size = mupdf.fz_lookup_base14_font(bfname)
if data:
font = mupdf.fz_new_font_from_memory(bfname, data, size, 0, 0)
font_obj = mupdf.pdf_add_simple_font(pdf, font, encoding)
exto = "n/a"
simple = 1
else:
if fontfile:
font = mupdf.fz_new_font_from_file(None, fontfile, idx, 0)
else:
res = JM_BufferFromBytes(fontbuffer)
if not res.m_internal:
RAISEPY(MSG_FILE_OR_BUFFER, PyExc_ValueError)
font = mupdf.fz_new_font_from_buffer(None, res, idx, 0)
if not set_simple:
font_obj = mupdf.pdf_add_cid_font(pdf, font)
simple = 0
else:
font_obj = mupdf.pdf_add_simple_font(pdf, font, encoding)
simple = 2
ixref = mupdf.pdf_to_num(font_obj)
name = JM_EscapeStrFromStr( mupdf.pdf_to_name( mupdf.pdf_dict_get(font_obj, PDF_NAME('BaseFont'))))
subt = JM_UnicodeFromStr( mupdf.pdf_to_name( mupdf.pdf_dict_get( font_obj, PDF_NAME('Subtype'))))
if not exto:
exto = JM_UnicodeFromStr(JM_get_fontextension(pdf, ixref))
asc = mupdf.fz_font_ascender(font)
dsc = mupdf.fz_font_descender(font)
value = [
ixref,
{
"name": name,
"type": subt,
"ext": exto,
"simple": bool(simple),
"ordering": ordering,
"ascender": asc,
"descender": dsc,
},
]
return value
def JM_invert_pixmap_rect( dest, b):
'''
invert a rectangle - also supports non-alpha pixmaps
'''
assert isinstance( dest, mupdf.FzPixmap)
assert isinstance( b, mupdf.FzIrect)
b = mupdf.fz_intersect_irect(b, mupdf.fz_pixmap_bbox( dest))
w = b.x1 - b.x0
y = b.y1 - b.y0
if w <= 0 or y <= 0:
return 0
destspan = dest.stride()
destp = destspan * (b.y0 - dest.y()) + dest.n() * (b.x0 - dest.x())
n0 = dest.n() - dest.alpha()
alpha = dest.alpha()
while 1:
s = destp
for x in range( w):
for i in range( n0):
ss = mupdf.fz_samples_get( dest, s)
ss = 255 - ss
mupdf.fz_samples_set( dest, s, ss)
s += 1
if alpha:
ss = mupdf.fz_samples_get( dest, s)
ss += 1
mupdf.fz_samples_set( dest, s, ss)
destp += destspan
y -= 1
if y == 0:
break
return 1
def JM_irect_from_py(r):
'''
PySequence to fz_irect. Default: infinite irect
'''
if isinstance(r, mupdf.FzIrect):
return r
if isinstance(r, IRect):
r = mupdf.FzIrect( r.x0, r.y0, r.x1, r.y1)
return r
if isinstance(r, Rect):
ret = mupdf.fz_make_irect(r.x0, r.y0, r.x1, r.y1)
return ret
if not r or not PySequence_Check(r) or PySequence_Size(r) != 4:
return mupdf.FzIrect(mupdf.fz_infinite_irect)
f = [0, 0, 0, 0]
for i in range(4):
f[i] = r[i]
if f[i] is None:
return mupdf.FzRect(mupdf.fz_infinite_irect)
if f[i] < FZ_MIN_INF_RECT:
f[i] = FZ_MIN_INF_RECT
if f[i] > FZ_MAX_INF_RECT:
f[i] = FZ_MAX_INF_RECT
return mupdf.fz_make_irect(f[0], f[1], f[2], f[3])
def JM_is_jbig2_image(dict_):
return 0
def JM_last_tree(arch, mount):
'''
Return last archive if it is a tree and mount points match
'''
assert 0, 'Not implemented'
def JM_listbox_value( annot):
'''
ListBox retrieve value
'''
annot_obj = mupdf.pdf_annot_obj( annot)
optarr = mupdf.pdf_dict_get( annot_obj, PDF_NAME('V'))
if mupdf.pdf_is_string( optarr):
return mupdf.pdf_to_text_string( optarr)
n = mupdf.pdf_array_len( optarr)
liste = []
for i in range( n):
elem = mupdf.pdf_array_get( optarr, i)
if mupdf.pdf_is_array( elem):
elem = mupdf.pdf_array_get( elem, 1)
liste.append( JM_UnicodeFromStr( mupdf.pdf_to_text_string( elem)))
return liste
def JM_make_annot_DA(annot, ncol, col, fontname, fontsize):
buf = ''
if ncol < 1:
buf += f'0 g '
elif ncol == 1:
buf += f'{col[0]:g} g '
elif ncol == 2:
assert 0
elif ncol == 3:
buf += f'{col[0]:g} {col[1]:g} {col[2]:g} rg '
else:
buf += f'{col[0]:g} {col[1]:g} {col[2]:g} {col[3]:g} k '
buf += f'/{JM_expand_fname(fontname)} {fontsize} Tf'
mupdf.pdf_dict_put_text_string(mupdf.pdf_annot_obj(annot), mupdf.PDF_ENUM_NAME_DA, buf)
def JM_make_spanlist(line_dict, line, raw, buff, tp_rect):
if g_use_extra:
return extra.JM_make_spanlist(line_dict, line, raw, buff, tp_rect)
char_list = None
span_list = []
mupdf.fz_clear_buffer(buff)
span_rect = mupdf.FzRect(mupdf.FzRect.Fixed_EMPTY)
line_rect = mupdf.FzRect(mupdf.FzRect.Fixed_EMPTY)
class char_style:
def __init__(self, rhs=None):
if rhs:
self.size = rhs.size
self.flags = rhs.flags
self.font = rhs.font
self.color = rhs.color
self.asc = rhs.asc
self.desc = rhs.desc
else:
self.size = -1
self.flags = -1
self.font = ''
self.color = -1
self.asc = 0
self.desc = 0
def __str__(self):
return f'{self.size} {self.flags} {self.font} {self.color} {self.asc} {self.desc}'
old_style = char_style()
style = char_style()
span = None
span_origin = None
for ch in line:
r = JM_char_bbox(line, ch)
if (not JM_rects_overlap(tp_rect, r)
and not mupdf.fz_is_infinite_rect(tp_rect)
):
continue
flags = JM_char_font_flags(mupdf.FzFont(mupdf.ll_fz_keep_font(ch.m_internal.font)), line, ch)
origin = mupdf.FzPoint(ch.m_internal.origin)
style.size = ch.m_internal.size
style.flags = flags
style.font = JM_font_name(mupdf.FzFont(mupdf.ll_fz_keep_font(ch.m_internal.font)))
style.color = ch.m_internal.color
style.asc = JM_font_ascender(mupdf.FzFont(mupdf.ll_fz_keep_font(ch.m_internal.font)))
style.desc = JM_font_descender(mupdf.FzFont(mupdf.ll_fz_keep_font(ch.m_internal.font)))
if (style.size != old_style.size
or style.flags != old_style.flags
or style.color != old_style.color
or style.font != old_style.font
):
if old_style.size >= 0:
if raw:
span[dictkey_chars] = char_list
char_list = None
else:
span[dictkey_text] = JM_EscapeStrFromBuffer( buff)
mupdf.fz_clear_buffer(buff)
span[dictkey_origin] = JM_py_from_point(span_origin)
span[dictkey_bbox] = JM_py_from_rect(span_rect)
line_rect = mupdf.fz_union_rect(line_rect, span_rect)
span_list.append( span)
span = None
span = dict()
asc = style.asc
desc = style.desc
if style.asc < 1e-3:
asc = 0.9
desc = -0.1
span[dictkey_size] = style.size
span[dictkey_flags] = style.flags
span[dictkey_font] = JM_EscapeStrFromStr(style.font)
span[dictkey_color] = style.color
span["ascender"] = asc
span["descender"] = desc
old_style = char_style(style)
span_rect = r
span_origin = origin
span_rect = mupdf.fz_union_rect(span_rect, r)
if raw:
char_dict = dict()
char_dict[dictkey_origin] = JM_py_from_point( ch.m_internal.origin)
char_dict[dictkey_bbox] = JM_py_from_rect(r)
char_dict[dictkey_c] = chr(ch.m_internal.c)
if char_list is None:
char_list = []
char_list.append(char_dict)
else:
JM_append_rune(buff, ch.m_internal.c)
if span:
if raw:
span[dictkey_chars] = char_list
char_list = None
else:
span[dictkey_text] = JM_EscapeStrFromBuffer(buff)
mupdf.fz_clear_buffer(buff)
span[dictkey_origin] = JM_py_from_point(span_origin)
span[dictkey_bbox] = JM_py_from_rect(span_rect)
if not mupdf.fz_is_empty_rect(span_rect):
span_list.append(span)
line_rect = mupdf.fz_union_rect(line_rect, span_rect)
span = None
if not mupdf.fz_is_empty_rect(line_rect):
line_dict[dictkey_spans] = span_list
else:
line_dict[dictkey_spans] = span_list
return line_rect
def JM_make_image_block(block, block_dict):
image = block.i_image()
n = mupdf.fz_colorspace_n(image.colorspace())
w = image.w()
h = image.h()
type_ = mupdf.FZ_IMAGE_UNKNOWN
ll_fz_compressed_buffer = mupdf.ll_fz_compressed_image_buffer(image.m_internal)
if ll_fz_compressed_buffer:
type_ = ll_fz_compressed_buffer.params.type
if type_ < mupdf.FZ_IMAGE_BMP or type_ == mupdf.FZ_IMAGE_JBIG2:
type_ = mupdf.FZ_IMAGE_UNKNOWN
bytes_ = None
if ll_fz_compressed_buffer and type_ != mupdf.FZ_IMAGE_UNKNOWN:
buf = mupdf.FzBuffer( mupdf.ll_fz_keep_buffer( ll_fz_compressed_buffer.buffer))
ext = JM_image_extension(type_)
else:
buf = mupdf.fz_new_buffer_from_image_as_png(image, mupdf.FzColorParams())
ext = "png"
bytes_ = JM_BinFromBuffer(buf)
block_dict[ dictkey_width] = w
block_dict[ dictkey_height] = h
block_dict[ dictkey_ext] = ext
block_dict[ dictkey_colorspace] = n
block_dict[ dictkey_xres] = image.xres()
block_dict[ dictkey_yres] = image.yres()
block_dict[ dictkey_bpc] = image.bpc()
block_dict[ dictkey_matrix] = JM_py_from_matrix(block.i_transform())
block_dict[ dictkey_size] = mupdf.fz_image_size( image)
block_dict[ dictkey_image] = bytes_
def JM_make_text_block(block, block_dict, raw, buff, tp_rect):
if g_use_extra:
return extra.JM_make_text_block(block.m_internal, block_dict, raw, buff.m_internal, tp_rect.m_internal)
line_list = []
block_rect = mupdf.FzRect(mupdf.FzRect.Fixed_EMPTY)
for line in block:
if (mupdf.fz_is_empty_rect(mupdf.fz_intersect_rect(tp_rect, mupdf.FzRect(line.m_internal.bbox)))
and not mupdf.fz_is_infinite_rect(tp_rect)
):
continue
line_dict = dict()
line_rect = JM_make_spanlist(line_dict, line, raw, buff, tp_rect)
block_rect = mupdf.fz_union_rect(block_rect, line_rect)
line_dict[dictkey_wmode] = line.m_internal.wmode
line_dict[dictkey_dir] = JM_py_from_point(line.m_internal.dir)
line_dict[dictkey_bbox] = JM_py_from_rect(line_rect)
line_list.append(line_dict)
block_dict[dictkey_bbox] = JM_py_from_rect(block_rect)
block_dict[dictkey_lines] = line_list
def JM_make_textpage_dict(tp, page_dict, raw):
if g_use_extra:
return extra.JM_make_textpage_dict(tp.m_internal, page_dict, raw)
text_buffer = mupdf.fz_new_buffer(128)
block_list = []
tp_rect = mupdf.FzRect(tp.m_internal.mediabox)
block_n = -1
for block in tp:
block_n += 1
if (not mupdf.fz_contains_rect(tp_rect, mupdf.FzRect(block.m_internal.bbox))
and not mupdf.fz_is_infinite_rect(tp_rect)
and block.m_internal.type == mupdf.FZ_STEXT_BLOCK_IMAGE
):
continue
if (not mupdf.fz_is_infinite_rect(tp_rect)
and mupdf.fz_is_empty_rect(mupdf.fz_intersect_rect(tp_rect, mupdf.FzRect(block.m_internal.bbox)))
):
continue
block_dict = dict()
block_dict[dictkey_number] = block_n
block_dict[dictkey_type] = block.m_internal.type
if block.m_internal.type == mupdf.FZ_STEXT_BLOCK_IMAGE:
block_dict[dictkey_bbox] = JM_py_from_rect(block.m_internal.bbox)
JM_make_image_block(block, block_dict)
else:
JM_make_text_block(block, block_dict, raw, text_buffer, tp_rect)
block_list.append(block_dict)
page_dict[dictkey_blocks] = block_list
def JM_matrix_from_py(m):
a = [0, 0, 0, 0, 0, 0]
if isinstance(m, mupdf.FzMatrix):
return m
if isinstance(m, Matrix):
return mupdf.FzMatrix(m.a, m.b, m.c, m.d, m.e, m.f)
if not m or not PySequence_Check(m) or PySequence_Size(m) != 6:
return mupdf.FzMatrix()
for i in range(6):
a[i] = JM_FLOAT_ITEM(m, i)
if a[i] is None:
return mupdf.FzRect()
return mupdf.FzMatrix(a[0], a[1], a[2], a[3], a[4], a[5])
def JM_mediabox(page_obj):
'''
return a PDF page's MediaBox
'''
page_mediabox = mupdf.FzRect(mupdf.FzRect.Fixed_UNIT)
mediabox = mupdf.pdf_to_rect(
mupdf.pdf_dict_get_inheritable(page_obj, PDF_NAME('MediaBox'))
)
if mupdf.fz_is_empty_rect(mediabox) or mupdf.fz_is_infinite_rect(mediabox):
mediabox.x0 = 0
mediabox.y0 = 0
mediabox.x1 = 612
mediabox.y1 = 792
page_mediabox = mupdf.FzRect(
mupdf.fz_min(mediabox.x0, mediabox.x1),
mupdf.fz_min(mediabox.y0, mediabox.y1),
mupdf.fz_max(mediabox.x0, mediabox.x1),
mupdf.fz_max(mediabox.y0, mediabox.y1),
)
if (page_mediabox.x1 - page_mediabox.x0 < 1
or page_mediabox.y1 - page_mediabox.y0 < 1
):
page_mediabox = mupdf.FzRect(mupdf.FzRect.Fixed_UNIT)
return page_mediabox
def JM_merge_range(
doc_des,
doc_src,
spage,
epage,
apage,
rotate,
links,
annots,
show_progress,
graft_map,
):
'''
Copy a range of pages (spage, epage) from a source PDF to a specified
location (apage) of the target PDF.
If spage > epage, the sequence of source pages is reversed.
'''
if g_use_extra:
return extra.JM_merge_range(
doc_des,
doc_src,
spage,
epage,
apage,
rotate,
links,
annots,
show_progress,
graft_map,
)
afterpage = apage
counter = 0
total = mupdf.fz_absi(epage - spage) + 1
if spage < epage:
page = spage
while page <= epage:
page_merge(doc_des, doc_src, page, afterpage, rotate, links, annots, graft_map)
counter += 1
if show_progress > 0 and counter % show_progress == 0:
sys.stdout.write("Inserted %i of %i pages.\n", counter, total)
page += 1
afterpage += 1
else:
page = spage
while page >= epage:
page_merge(doc_des, doc_src, page, afterpage, rotate, links, annots, graft_map)
counter += 1
if show_progress > 0 and counter % show_progress == 0:
sys.stdout.write("Inserted %i of %i pages.\n", counter, total)
page -= 1
afterpage += 1
def JM_merge_resources( page, temp_res):
'''
Merge the /Resources object created by a text pdf device into the page.
The device may have created multiple /ExtGState/Alp? and /Font/F? objects.
These need to be renamed (renumbered) to not overwrite existing page
objects from previous executions.
Returns the next available numbers n, m for objects /Alp<n>, /F<m>.
'''
resources = mupdf.pdf_dict_get(page.obj(), PDF_NAME('Resources'))
main_extg = mupdf.pdf_dict_get(resources, PDF_NAME('ExtGState'))
main_fonts = mupdf.pdf_dict_get(resources, PDF_NAME('Font'))
temp_extg = mupdf.pdf_dict_get(temp_res, PDF_NAME('ExtGState'))
temp_fonts = mupdf.pdf_dict_get(temp_res, PDF_NAME('Font'))
max_alp = -1
max_fonts = -1
if mupdf.pdf_is_dict(temp_extg):
n = mupdf.pdf_dict_len(temp_extg)
if mupdf.pdf_is_dict(main_extg):
for i in range(mupdf.pdf_dict_len(main_extg)):
alp = mupdf.pdf_to_name( mupdf.pdf_dict_get_key(main_extg, i))
if not alp.startswith('Alp'):
continue
j = mupdf.fz_atoi(alp[3:])
if j > max_alp:
max_alp = j
else:
main_extg = mupdf.pdf_dict_put_dict(resources, PDF_NAME('ExtGState'), n)
max_alp += 1
for i in range(n):
alp = mupdf.pdf_to_name( mupdf.pdf_dict_get_key( temp_extg, i))
j = mupdf.fz_atoi(alp[3:]) + max_alp
text = f'Alp{j}'
val = mupdf.pdf_dict_get_val( temp_extg, i)
mupdf.pdf_dict_puts(main_extg, text, val)
if mupdf.pdf_is_dict(main_fonts):
for i in range(mupdf.pdf_dict_len(main_fonts)):
font = mupdf.pdf_to_name( mupdf.pdf_dict_get_key( main_fonts, i))
if not font.startswith("F"):
continue
j = mupdf.fz_atoi(font[1:])
if j > max_fonts:
max_fonts = j
else:
main_fonts = mupdf.pdf_dict_put_dict(resources, PDF_NAME('Font'), 2)
max_fonts += 1
for i in range(mupdf.pdf_dict_len(temp_fonts)):
font = mupdf.pdf_to_name( mupdf.pdf_dict_get_key( temp_fonts, i))
j = mupdf.fz_atoi(font[1:]) + max_fonts
text = f'F{j}'
val = mupdf.pdf_dict_get_val(temp_fonts, i)
mupdf.pdf_dict_puts(main_fonts, text, val)
return (max_alp, max_fonts)
def JM_mupdf_warning( message):
'''
redirect MuPDF warnings
'''
sys.stderr.flush()
JM_mupdf_warnings_store.append(message)
if JM_mupdf_show_warnings:
sys.stderr.write(f'MuPDF warning: {message}\n')
def JM_mupdf_error( message):
JM_mupdf_warnings_store.append(message)
if JM_mupdf_show_errors:
sys.stderr.write(f'MuPDF error: {message}\n')
def JM_new_bbox_device(rc, inc_layers):
assert isinstance(rc, list)
return JM_new_bbox_device_Device( rc, inc_layers)
def JM_new_buffer_from_stext_page(page):
'''
make a buffer from an stext_page's text
'''
assert isinstance(page, mupdf.FzStextPage)
rect = mupdf.FzRect(page.m_internal.mediabox)
buf = mupdf.fz_new_buffer(256)
for block in page:
if block.m_internal.type == mupdf.FZ_STEXT_BLOCK_TEXT:
for line in block:
for ch in line:
if (not JM_rects_overlap(rect, JM_char_bbox(line, ch))
and not mupdf.fz_is_infinite_rect(rect)
):
continue
mupdf.fz_append_rune(buf, ch.m_internal.c)
mupdf.fz_append_byte(buf, ord('\n'))
mupdf.fz_append_byte(buf, ord('\n'))
return buf
def JM_new_javascript(pdf, value):
'''
make new PDF action object from JavaScript source
Parameters are a PDF document and a Python string.
Returns a PDF action object.
'''
if value is None:
return
data = JM_StrAsChar(value)
if data is None:
return
res = mupdf.fz_new_buffer_from_copied_data(data)
source = mupdf.pdf_add_stream(pdf, res, mupdf.PdfObj(), 0)
newaction = mupdf.pdf_add_new_dict(pdf, 4)
mupdf.pdf_dict_put(newaction, PDF_NAME('S'), mupdf.pdf_new_name('JavaScript'))
mupdf.pdf_dict_put(newaction, PDF_NAME('JS'), source)
return newaction
def JM_new_output_fileptr(bio):
return JM_new_output_fileptr_Output( bio)
def JM_norm_rotation(rotate):
'''
# return normalized /Rotate value:one of 0, 90, 180, 270
'''
while rotate < 0:
rotate += 360
while rotate >= 360:
rotate -= 360
if rotate % 90 != 0:
return 0
return rotate
def JM_object_to_buffer(what, compress, ascii):
res = mupdf.fz_new_buffer(512)
out = mupdf.FzOutput(res)
mupdf.pdf_print_obj(out, what, compress, ascii)
mupdf.fz_terminate_buffer(res)
return res
def JM_outline_xrefs(obj, xrefs):
'''
Return list of outline xref numbers. Recursive function. Arguments:
'obj' first OL item
'xrefs' empty Python list
'''
if not obj.m_internal:
return xrefs
thisobj = obj
while thisobj.m_internal:
newxref = mupdf.pdf_to_num( thisobj)
if newxref in xrefs or mupdf.pdf_dict_get( thisobj, PDF_NAME('Type')).m_internal:
break
xrefs.append( newxref)
first = mupdf.pdf_dict_get( thisobj, PDF_NAME('First'))
if mupdf.pdf_is_dict( first):
xrefs = JM_outline_xrefs( first, xrefs)
thisobj = mupdf.pdf_dict_get( thisobj, PDF_NAME('Next'))
parent = mupdf.pdf_dict_get( thisobj, PDF_NAME('Parent'))
if not mupdf.pdf_is_dict( thisobj):
thisobj = parent
return xrefs
def JM_page_rotation(page):
'''
return a PDF page's /Rotate value: one of (0, 90, 180, 270)
'''
rotate = 0
obj = mupdf.pdf_dict_get_inheritable( page.obj(), mupdf.PDF_ENUM_NAME_Rotate)
rotate = mupdf.pdf_to_int(obj)
rotate = JM_norm_rotation(rotate)
return rotate
def JM_pdf_obj_from_str(doc, src):
'''
create PDF object from given string (new in v1.14.0: MuPDF dropped it)
'''
buffer_ = mupdf.fz_new_buffer_from_copied_data(bytes(src, 'utf8'))
stream = mupdf.fz_open_buffer(buffer_)
lexbuf = mupdf.PdfLexbuf(mupdf.PDF_LEXBUF_SMALL)
result = mupdf.pdf_parse_stm_obj(doc, stream, lexbuf)
return result
def JM_pixmap_from_display_list(
list_,
ctm,
cs,
alpha,
clip,
seps,
):
'''
Version of fz_new_pixmap_from_display_list (util.c) to also support
rendering of only the 'clip' part of the displaylist rectangle
'''
assert isinstance(list_, mupdf.FzDisplayList)
if seps is None:
seps = mupdf.FzSeparations()
assert seps is None or isinstance(seps, mupdf.FzSeparations), f'{type(seps)=}: {seps}'
rect = mupdf.fz_bound_display_list(list_)
matrix = JM_matrix_from_py(ctm)
rclip = JM_rect_from_py(clip)
rect = mupdf.fz_intersect_rect(rect, rclip)
rect = mupdf.fz_transform_rect(rect, matrix)
irect = mupdf.fz_round_rect(rect)
assert isinstance( cs, Colorspace)
pix = mupdf.fz_new_pixmap_with_bbox(cs.this, irect, seps, alpha)
if alpha:
mupdf.fz_clear_pixmap(pix)
else:
mupdf.fz_clear_pixmap_with_value(pix, 0xFF)
if not mupdf.fz_is_infinite_rect(rclip):
dev = mupdf.fz_new_draw_device_with_bbox(matrix, pix, irect)
mupdf.fz_run_display_list(list_, dev, mupdf.FzMatrix(), rclip, mupdf.FzCookie())
else:
dev = mupdf.fz_new_draw_device(matrix, pix)
mupdf.fz_run_display_list(list_, dev, mupdf.FzMatrix(), mupdf.FzRect(mupdf.FzRect.Fixed_INFINITE), mupdf.FzCookie())
mupdf.fz_close_device(dev)
return Pixmap( 'raw', pix)
def JM_point_from_py(p):
'''
PySequence to fz_point. Default: (FZ_MIN_INF_RECT, FZ_MIN_INF_RECT)
'''
if isinstance(p, mupdf.FzPoint):
return p
if isinstance(p, Point):
return mupdf.FzPoint(p.x, p.y)
if g_use_extra:
return extra.JM_point_from_py( p)
p0 = mupdf.FzPoint(0, 0)
x = JM_FLOAT_ITEM(p, 0)
y = JM_FLOAT_ITEM(p, 1)
if x is None or y is None:
return p0
x = max( x, FZ_MIN_INF_RECT)
y = max( y, FZ_MIN_INF_RECT)
x = min( x, FZ_MAX_INF_RECT)
y = min( y, FZ_MAX_INF_RECT)
return mupdf.FzPoint(x, y)
def JM_print_stext_page_as_text(res, page):
'''
Plain text output. An identical copy of fz_print_stext_page_as_text,
but lines within a block are concatenated by space instead a new-line
character (which else leads to 2 new-lines).
'''
if 1 and g_use_extra:
return extra.JM_print_stext_page_as_text(res, page)
assert isinstance(res, mupdf.FzBuffer)
assert isinstance(page, mupdf.FzStextPage)
rect = mupdf.FzRect(page.m_internal.mediabox)
last_char = 0
n_blocks = 0
n_lines = 0
n_chars = 0
for n_blocks2, block in enumerate( page):
if block.m_internal.type == mupdf.FZ_STEXT_BLOCK_TEXT:
for n_lines2, line in enumerate( block):
for n_chars2, ch in enumerate( line):
pass
n_chars += n_chars2
n_lines += n_lines2
n_blocks += n_blocks2
for block in page:
if block.m_internal.type == mupdf.FZ_STEXT_BLOCK_TEXT:
for line in block:
last_char = 0
for ch in line:
chbbox = JM_char_bbox(line, ch)
if (mupdf.fz_is_infinite_rect(rect)
or JM_rects_overlap(rect, chbbox)
):
last_char = ch.m_internal.c
JM_append_rune(res, last_char)
if last_char != 10 and last_char > 0:
mupdf.fz_append_string(res, "\n")
def JM_put_script(annot_obj, key1, key2, value):
'''
Create a JavaScript PDF action.
Usable for all object types which support PDF actions, even if the
argument name suggests annotations. Up to 2 key values can be specified, so
JavaScript actions can be stored for '/A' and '/AA/?' keys.
'''
key1_obj = mupdf.pdf_dict_get(annot_obj, key1)
pdf = mupdf.pdf_get_bound_document(annot_obj)
if not value:
if not key2 or not key2.m_internal:
mupdf.pdf_dict_del(annot_obj, key1)
elif key1_obj.m_internal:
mupdf.pdf_dict_del(key1_obj, key2)
return
if not key2.m_internal or not key1_obj.m_internal:
script = JM_get_script(key1_obj)
else:
script = JM_get_script(mupdf.pdf_dict_get(key1_obj, key2))
if value != script:
newaction = JM_new_javascript(pdf, value)
if not key2.m_internal:
mupdf.pdf_dict_put(annot_obj, key1, newaction)
else:
mupdf.pdf_dict_putl(annot_obj, newaction, key1, key2)
def JM_py_from_irect(r):
return r.x0, r.y0, r.x1, r.y1
def JM_py_from_matrix(m):
return m.a, m.b, m.c, m.d, m.e, m.f
def JM_py_from_point(p):
return p.x, p.y
def JM_py_from_quad(q):
'''
PySequence from fz_quad.
'''
return (
(q.ul.x, q.ul.y),
(q.ur.x, q.ur.y),
(q.ll.x, q.ll.y),
(q.lr.x, q.lr.y),
)
def JM_py_from_rect(r):
return r.x0, r.y0, r.x1, r.y1
def JM_quad_from_py(r):
if isinstance(r, mupdf.FzQuad):
return r
if isinstance( r, Rect):
r = mupdf.FzRect( r.x0, r.y0, r.x1, r.y1)
if isinstance( r, mupdf.FzRect):
return mupdf.fz_quad_from_rect( r)
if isinstance( r, Quad):
return mupdf.fz_make_quad(
r.ul.x, r.ul.y,
r.ur.x, r.ur.y,
r.ll.x, r.ll.y,
r.lr.x, r.lr.y,
)
q = mupdf.fz_make_quad(0, 0, 0, 0, 0, 0, 0, 0)
p = [0,0,0,0]
if not r or not isinstance(r, (tuple, list)) or len(r) != 4:
return q
if JM_FLOAT_ITEM(r, 0) is None:
return mupdf.fz_quad_from_rect(JM_rect_from_py(r))
for i in range(4):
obj = r[i]
if not obj.m_internal or not PySequence_Check(obj) or PySequence_Size(obj) != 2:
return q
p[i].x = JM_FLOAT_ITEM(obj, 0)
p[i].y = JM_FLOAT_ITEM(obj, 1)
if p[i].x is None or p[i].y is None:
return q
p[i].x = max( p[i].x, FZ_MIN_INF_RECT)
p[i].y = max( p[i].y, FZ_MIN_INF_RECT)
p[i].x = min( p[i].x, FZ_MAX_INF_RECT)
p[i].y = min( p[i].y, FZ_MAX_INF_RECT)
q.ul = p[0]
q.ur = p[1]
q.ll = p[2]
q.lr = p[3]
return q
def JM_read_contents(pageref):
'''
Read and concatenate a PDF page's /Conents object(s) in a buffer
'''
assert isinstance(pageref, mupdf.PdfObj), f'{type(pageref)}'
contents = mupdf.pdf_dict_get(pageref, mupdf.PDF_ENUM_NAME_Contents)
if mupdf.pdf_is_array(contents):
res = mupdf.FzBuffer(1024)
for i in range(mupdf.pdf_array_len(contents)):
obj = mupdf.pdf_array_get(contents, i)
nres = mupdf.pdf_load_stream(obj)
mupdf.fz_append_buffer(res, nres)
elif contents.m_internal:
res = mupdf.pdf_load_stream(contents)
return res
def JM_rect_from_py(r):
if isinstance(r, mupdf.FzRect):
return r
if isinstance(r, Rect):
return mupdf.fz_make_rect(r.x0, r.y0, r.x1, r.y1)
if not r or not PySequence_Check(r) or PySequence_Size(r) != 4:
return mupdf.FzRect(mupdf.FzRect.Fixed_INFINITE)
f = [0, 0, 0, 0]
for i in range(4):
f[i] = JM_FLOAT_ITEM(r, i)
if f[i] is None:
return mupdf.FzRect(mupdf.FzRect.Fixed_INFINITE)
if f[i] < FZ_MIN_INF_RECT:
f[i] = FZ_MIN_INF_RECT
if f[i] > FZ_MAX_INF_RECT:
f[i] = FZ_MAX_INF_RECT
return mupdf.fz_make_rect(f[0], f[1], f[2], f[3])
def JM_rects_overlap(a, b):
if (0
or a.x0 >= b.x1
or a.y0 >= b.y1
or a.x1 <= b.x0
or a.y1 <= b.y0
):
return 0
return 1
def JM_refresh_links( page):
'''
refreshes the link and annotation tables of a page
'''
if not page:
return
obj = mupdf.pdf_dict_get( page.obj(), PDF_NAME('Annots'))
if obj.m_internal:
pdf = page.doc()
number = mupdf.pdf_lookup_page_number( pdf, page.obj())
page_mediabox = mupdf.FzRect()
page_ctm = mupdf.FzMatrix()
mupdf.pdf_page_transform( page, page_mediabox, page_ctm)
link = mupdf.pdf_load_link_annots( pdf, page, obj, number, page_ctm)
page.m_internal.links = mupdf.ll_fz_keep_link( link.m_internal)
def JM_rotate_page_matrix(page):
'''
calculate page rotation matrices
'''
if not page.m_internal:
return mupdf.FzMatrix()
rotation = JM_page_rotation(page)
if rotation == 0:
return mupdf.FzMatrix()
cb_size = JM_cropbox_size(page.obj())
w = cb_size.x
h = cb_size.y
if rotation == 90:
m = mupdf.fz_make_matrix(0, 1, -1, 0, h, 0)
elif rotation == 180:
m = mupdf.fz_make_matrix(-1, 0, 0, -1, w, h)
else:
m = mupdf.fz_make_matrix(0, -1, 1, 0, 0, w)
return m
def JM_search_stext_page(page, needle):
if g_use_extra:
return extra.JM_search_stext_page(page.m_internal, needle)
rect = mupdf.FzRect(page.m_internal.mediabox)
if not needle:
return
quads = []
class Hits:
def __str__(self):
return f'Hits(len={self.len} quads={self.quads} hfuzz={self.hfuzz} vfuzz={self.vfuzz}'
hits = Hits()
hits.len = 0
hits.quads = quads
hits.hfuzz = 0.2
hits.vfuzz = 0.1
buffer_ = JM_new_buffer_from_stext_page(page)
haystack_string = mupdf.fz_string_from_buffer(buffer_)
haystack = 0
begin, end = find_string(haystack_string[haystack:], needle)
if begin is None:
return quads
begin += haystack
end += haystack
inside = 0
i = 0
for block in page:
if block.m_internal.type != mupdf.FZ_STEXT_BLOCK_TEXT:
continue
for line in block:
for ch in line:
i += 1
if not mupdf.fz_is_infinite_rect(rect):
r = JM_char_bbox(line, ch)
if not JM_rects_overlap(rect, r):
continue
while 1:
if not inside:
if haystack >= begin:
inside = 1
if inside:
if haystack < end:
on_highlight_char(hits, line, ch)
break
else:
inside = 0
begin, end = find_string(haystack_string[haystack:], needle)
if begin is None:
return quads
else:
begin += haystack
end += haystack
continue
break
haystack += 1
assert haystack_string[haystack] == '\n', \
f'{haystack=} {haystack_string[haystack]=}'
haystack += 1
assert haystack_string[haystack] == '\n', \
f'{haystack=} {haystack_string[haystack]=}'
haystack += 1
return quads
def JM_scan_resources(pdf, rsrc, liste, what, stream_xref, tracer):
'''
Step through /Resources, looking up image, xobject or font information
'''
if mupdf.pdf_mark_obj(rsrc):
mupdf.fz_warn('Circular dependencies! Consider page cleaning.')
return
try:
xobj = mupdf.pdf_dict_get(rsrc, mupdf.PDF_ENUM_NAME_XObject)
if what == 1:
font = mupdf.pdf_dict_get(rsrc, mupdf.PDF_ENUM_NAME_Font)
JM_gather_fonts(pdf, font, liste, stream_xref)
elif what == 2:
JM_gather_images(pdf, xobj, liste, stream_xref)
elif what == 3:
JM_gather_forms(pdf, xobj, liste, stream_xref)
else:
return
n = mupdf.pdf_dict_len(xobj)
for i in range(n):
obj = mupdf.pdf_dict_get_val(xobj, i)
if mupdf.pdf_is_stream(obj):
sxref = mupdf.pdf_to_num(obj)
else:
sxref = 0
subrsrc = mupdf.pdf_dict_get(obj, mupdf.PDF_ENUM_NAME_Resources)
if subrsrc.m_internal:
sxref_t = sxref
if sxref_t not in tracer:
tracer.append(sxref_t)
JM_scan_resources( pdf, subrsrc, liste, what, sxref, tracer)
else:
mupdf.fz_warn('Circular dependencies! Consider page cleaning.')
return
finally:
mupdf.pdf_unmark_obj(rsrc)
def JM_set_choice_options(annot, liste):
'''
set ListBox / ComboBox values
'''
if not liste:
return
assert isinstance( liste, (tuple, list))
n = len( liste)
if n == 0:
return
annot_obj = mupdf.pdf_annot_obj( annot)
pdf = mupdf.pdf_get_bound_document( annot_obj)
optarr = mupdf.pdf_new_array( pdf, n)
for i in range(n):
val = liste[i]
opt = val
if isinstance(opt, str):
mupdf.pdf_array_push_text_string( optarr, opt)
else:
assert isinstance( val, (tuple, list)) and len( val) == 2, 'bad choice field list'
opt1, opt2 = val
assert opt1 and opt2, 'bad choice field list'
optarrsub = mupdf.pdf_array_push_array( optarr, 2)
mupdf.pdf_array_push_text_string( optarrsub, opt1)
mupdf.pdf_array_push_text_string( optarrsub, opt2)
mupdf.pdf_dict_put( annot_obj, PDF_NAME('Opt'), optarr)
def JM_set_field_type(doc, obj, type):
'''
Set the field type
'''
setbits = 0
clearbits = 0
typename = None
if type == mupdf.PDF_WIDGET_TYPE_BUTTON:
typename = PDF_NAME('Btn')
setbits = mupdf.PDF_BTN_FIELD_IS_PUSHBUTTON
elif type == mupdf.PDF_WIDGET_TYPE_RADIOBUTTON:
typename = PDF_NAME('Btn')
clearbits = mupdf.PDF_BTN_FIELD_IS_PUSHBUTTON
setbits = mupdf.PDF_BTN_FIELD_IS_RADIO
elif type == mupdf.PDF_WIDGET_TYPE_CHECKBOX:
typename = PDF_NAME('Btn')
clearbits = (mupdf.PDF_BTN_FIELD_IS_PUSHBUTTON | mupdf.PDF_BTN_FIELD_IS_RADIO)
elif type == mupdf.PDF_WIDGET_TYPE_TEXT:
typename = PDF_NAME('Tx')
elif type == mupdf.PDF_WIDGET_TYPE_LISTBOX:
typename = PDF_NAME('Ch')
clearbits = mupdf.PDF_CH_FIELD_IS_COMBO
elif type == mupdf.PDF_WIDGET_TYPE_COMBOBOX:
typename = PDF_NAME('Ch')
setbits = mupdf.PDF_CH_FIELD_IS_COMBO
elif type == mupdf.PDF_WIDGET_TYPE_SIGNATURE:
typename = PDF_NAME('Sig')
if typename:
mupdf.pdf_dict_put(obj, PDF_NAME('FT'), typename)
if setbits != 0 or clearbits != 0:
bits = mupdf.pdf_dict_get_int(obj, PDF_NAME('Ff'))
bits &= ~clearbits
bits |= setbits
mupdf.pdf_dict_put_int(obj, PDF_NAME('Ff'), bits)
def JM_set_object_value(obj, key, value):
'''
Set a PDF dict key to some value
'''
eyecatcher = "fitz: replace me!"
pdf = mupdf.pdf_get_bound_document(obj)
list_ = key.split('/')
len_ = len(list_)
i = len_ - 1
skey = list_[i]
del list_[i]
len_ = len(list_)
testkey = mupdf.pdf_dict_getp(obj, key)
if not testkey.m_internal:
while len_ > 0:
t = '/'.join(list_)
if mupdf.pdf_is_indirect(mupdf.pdf_dict_getp(obj, JM_StrAsChar(t))):
raise Exception("path to '%s' has indirects", JM_StrAsChar(skey))
del list_[len_ - 1]
len_ = len(list_)
mupdf.pdf_dict_putp(obj, key, mupdf.pdf_new_text_string(eyecatcher))
testkey = mupdf.pdf_dict_getp(obj, key)
if not mupdf.pdf_is_string(testkey):
raise Exception("cannot insert value for '%s'", key)
temp = mupdf.pdf_to_text_string(testkey)
if temp != eyecatcher:
raise Exception("cannot insert value for '%s'", key)
res = JM_object_to_buffer(obj, 1, 0)
objstr = JM_EscapeStrFromBuffer(res)
nullval = "/%s(%s)" % ( skey, eyecatcher)
newval = "/%s %s" % (skey, value)
newstr = objstr.replace(nullval, newval, 1)
new_obj = JM_pdf_obj_from_str(pdf, newstr)
return new_obj
def JM_set_ocg_arrays(conf, basestate, on, off, rbgroups, locked):
if basestate:
mupdf.pdf_dict_put_name( conf, PDF_NAME('BaseState'), basestate)
if on is not None:
mupdf.pdf_dict_del( conf, PDF_NAME('ON'))
if on:
arr = mupdf.pdf_dict_put_array( conf, PDF_NAME('ON'), 1)
JM_set_ocg_arrays_imp( arr, on)
if off is not None:
mupdf.pdf_dict_del( conf, PDF_NAME('OFF'))
if off:
arr = mupdf.pdf_dict_put_array( conf, PDF_NAME('OFF'), 1)
JM_set_ocg_arrays_imp( arr, off)
if locked is not None:
mupdf.pdf_dict_del( conf, PDF_NAME('Locked'))
if locked:
arr = mupdf.pdf_dict_put_array( conf, PDF_NAME('Locked'), 1)
JM_set_ocg_arrays_imp( arr, locked)
if rbgroups is not None:
mupdf.pdf_dict_del( conf, PDF_NAME('RBGroups'))
if rbgroups:
arr = mupdf.pdf_dict_put_array( conf, PDF_NAME('RBGroups'), 1)
n =len(rbgroups)
for i in range(n):
item0 = rbgroups[i]
obj = mupdf.pdf_array_push_array( arr, 1)
JM_set_ocg_arrays_imp( obj, item0)
def JM_set_ocg_arrays_imp(arr, list_):
'''
Set OCG arrays from dict of Python lists
Works with dict like {"basestate":name, "on":list, "off":list, "rbg":list}
'''
pdf = mupdf.pdf_get_bound_document(arr)
for i, item in enumerate(list_):
xref = 0
if JM_INT_ITEM(list_, i)[0] == 1:
continue
obj = mupdf.pdf_new_indirect(pdf, xref, 0)
mupdf.pdf_array_push_drop(arr, obj)
def JM_set_resource_property(ref, name, xref):
'''
Insert an item into Resources/Properties (used for Marked Content)
Arguments:
(1) e.g. page object, Form XObject
(2) marked content name
(3) xref of the referenced object (insert as indirect reference)
'''
pdf = mupdf.pdf_get_bound_document(ref)
ind = mupdf.pdf_new_indirect(pdf, xref, 0)
if not ind.m_internal:
RAISEPY(MSG_BAD_XREF, PyExc_ValueError)
resources = mupdf.pdf_dict_get(ref, PDF_NAME('Resources'))
if not resources.m_internal:
resources = mupdf.pdf_dict_put_dict(ref, PDF_NAME('Resources'), 1)
properties = mupdf.pdf_dict_get(resources, PDF_NAME('Properties'))
if not properties.m_internal:
properties = mupdf.pdf_dict_put_dict(resources, PDF_NAME('Properties'), 1)
mupdf.pdf_dict_put(properties, mupdf.pdf_new_name(name), ind)
def JM_set_widget_properties(annot, Widget):
'''
Update the PDF form field with the properties from a Python Widget object.
Called by "Page.addWidget" and "Annot.updateWidget".
'''
if isinstance( annot, Annot):
annot = annot.this
assert isinstance( annot, mupdf.PdfAnnot), f'{type(annot)=} {type=}'
page = mupdf.pdf_annot_page(annot)
annot_obj = mupdf.pdf_annot_obj(annot)
pdf = page.doc()
def GETATTR(name):
return getattr(Widget, name, None)
value = GETATTR("field_type")
field_type = value
value = GETATTR("rect")
rect = JM_rect_from_py(value)
rot_mat = JM_rotate_page_matrix(page)
rect = mupdf.fz_transform_rect(rect, rot_mat)
mupdf.pdf_set_annot_rect(annot, rect)
value = GETATTR("fill_color")
if value and PySequence_Check(value):
n = len(value)
fill_col = mupdf.pdf_new_array(pdf, n)
col = 0
for i in range(n):
col = value[i]
mupdf.pdf_array_push_real(fill_col, col)
mupdf.pdf_field_set_fill_color(annot_obj, fill_col)
value = GETATTR("border_dashes")
if value and PySequence_Check(value):
n = len(value)
dashes = mupdf.pdf_new_array(pdf, n)
for i in range(n):
mupdf.pdf_array_push_int(dashes, value[i])
mupdf.pdf_dict_putl(annot_obj, dashes, PDF_NAME('BS'), PDF_NAME('D'))
value = GETATTR("border_color")
if value and PySequence_Check(value):
n = len(value)
border_col = mupdf.pdf_new_array(pdf, n)
col = 0
for i in range(n):
col = value[i]
mupdf.pdf_array_push_real(border_col, col)
mupdf.pdf_dict_putl(annot_obj, border_col, PDF_NAME('MK'), PDF_NAME('BC'))
value = GETATTR("field_label")
if value is not None:
label = JM_StrAsChar(value)
mupdf.pdf_dict_put_text_string(annot_obj, PDF_NAME('TU'), label)
value = GETATTR("field_name")
if value is not None:
name = JM_StrAsChar(value)
old_name = mupdf.pdf_load_field_name(annot_obj)
if name != old_name:
mupdf.pdf_dict_put_text_string(annot_obj, PDF_NAME('T'), name)
if field_type == mupdf.PDF_WIDGET_TYPE_TEXT:
value = GETATTR("text_maxlen")
text_maxlen = value
if text_maxlen:
mupdf.pdf_dict_put_int(annot_obj, PDF_NAME('MaxLen'), text_maxlen)
value = GETATTR("field_display")
d = value
mupdf.pdf_field_set_display(annot_obj, d)
if field_type in (mupdf.PDF_WIDGET_TYPE_LISTBOX, mupdf.PDF_WIDGET_TYPE_COMBOBOX):
value = GETATTR("choice_values")
JM_set_choice_options(annot, value)
value = GETATTR("border_style")
val = JM_get_border_style(value)
mupdf.pdf_dict_putl(annot_obj, val, PDF_NAME('BS'), PDF_NAME('S'))
value = GETATTR("border_width")
border_width = value
mupdf.pdf_dict_putl(
annot_obj,
mupdf.pdf_new_real(border_width),
PDF_NAME('BS'),
PDF_NAME('W'),
)
value = GETATTR("_text_da")
da = JM_StrAsChar(value)
mupdf.pdf_dict_put_text_string(annot_obj, PDF_NAME('DA'), da)
mupdf.pdf_dict_del(annot_obj, PDF_NAME('DS'))
mupdf.pdf_dict_del(annot_obj, PDF_NAME('RC'))
field_flags = GETATTR("field_flags")
if field_flags is not None:
if field_type == mupdf.PDF_WIDGET_TYPE_COMBOBOX:
field_flags |= mupdf.PDF_CH_FIELD_IS_COMBO
elif field_type == mupdf.PDF_WIDGET_TYPE_RADIOBUTTON:
field_flags |= mupdf.PDF_BTN_FIELD_IS_RADIO
elif field_type == mupdf.PDF_WIDGET_TYPE_BUTTON:
field_flags |= mupdf.PDF_BTN_FIELD_IS_PUSHBUTTON
mupdf.pdf_dict_put_int( annot_obj, PDF_NAME('Ff'), field_flags)
value = GETATTR("button_caption")
ca = JM_StrAsChar(value)
if ca:
mupdf.pdf_field_set_button_caption(annot_obj, ca)
value = GETATTR("script")
JM_put_script(annot_obj, PDF_NAME('A'), None, value)
value = GETATTR("script_stroke")
JM_put_script(annot_obj, PDF_NAME('AA'), PDF_NAME('K'), value)
value = GETATTR("script_format")
JM_put_script(annot_obj, PDF_NAME('AA'), PDF_NAME('F'), value)
value = GETATTR("script_change")
JM_put_script(annot_obj, PDF_NAME('AA'), PDF_NAME('V'), value)
value = GETATTR("script_calc")
JM_put_script(annot_obj, PDF_NAME('AA'), PDF_NAME('C'), value)
value = GETATTR("script_blur")
JM_put_script(annot_obj, PDF_NAME('AA'), mupdf.pdf_new_name('Bl'), value)
value = GETATTR("script_focus")
JM_put_script(annot_obj, PDF_NAME('AA'), mupdf.pdf_new_name('Fo'), value)
value = GETATTR("field_value")
text = JM_StrAsChar(value)
if field_type == mupdf.PDF_WIDGET_TYPE_RADIOBUTTON:
if not value:
mupdf.pdf_set_field_value(pdf, annot_obj, "Off", 1)
mupdf.pdf_dict_put_name(annot_obj, PDF_NAME('AS'), "Off")
else:
onstate = mupdf.pdf_button_field_on_state(annot_obj)
if onstate.m_internal:
on = mupdf.pdf_to_name(onstate)
mupdf.pdf_set_field_value(pdf, annot_obj, on, 1)
mupdf.pdf_dict_put_name(annot_obj, PDF_NAME('AS'), on)
elif text:
mupdf.pdf_dict_put_name(annot_obj, PDF_NAME('AS'), text)
elif field_type == mupdf.PDF_WIDGET_TYPE_CHECKBOX:
if value is True or text == 'Yes':
onstate = mupdf.pdf_button_field_on_state(annot_obj)
on = mupdf.pdf_to_name(onstate)
mupdf.pdf_set_field_value(pdf, annot_obj, on, 1)
mupdf.pdf_dict_put_name(annot_obj, PDF_NAME('AS'), 'Yes')
mupdf.pdf_dict_put_name(annot_obj, PDF_NAME('V'), 'Yes')
else:
mupdf.pdf_dict_put_name( annot_obj, PDF_NAME('AS'), 'Off')
mupdf.pdf_dict_put_name( annot_obj, PDF_NAME('V'), 'Off')
else:
if text:
mupdf.pdf_set_field_value(pdf, annot_obj, text, 1)
if field_type in (mupdf.PDF_WIDGET_TYPE_COMBOBOX, mupdf.PDF_WIDGET_TYPE_LISTBOX):
mupdf.pdf_dict_del(annot_obj, PDF_NAME('I'))
mupdf.pdf_dirty_annot(annot)
mupdf.pdf_set_annot_hot(annot, 1)
mupdf.pdf_set_annot_active(annot, 1)
mupdf.pdf_update_annot(annot)
def JM_show_string_cs(
text,
user_font,
trm,
s,
wmode,
bidi_level,
markup_dir,
language,
):
i = 0
while i < len(s):
l, ucs = mupdf.fz_chartorune(s[i:])
i += l
gid = mupdf.fz_encode_character_sc(user_font, ucs)
if gid == 0:
gid, font = mupdf.fz_encode_character_with_fallback(user_font, ucs, 0, language)
else:
font = user_font
mupdf.fz_show_glyph(text, font, trm, gid, ucs, wmode, bidi_level, markup_dir, language)
adv = mupdf.fz_advance_glyph(font, gid, wmode)
if wmode == 0:
trm = mupdf.fz_pre_translate(trm, adv, 0)
else:
trm = mupdf.fz_pre_translate(trm, 0, -adv)
return trm
def JM_UnicodeFromBuffer(buff):
buff_bytes = mupdf.fz_buffer_extract_copy(buff)
val = buff_bytes.decode(errors='replace')
z = val.find(chr(0))
if z >= 0:
val = val[:z]
return val
def JM_Warning(id):
'''
put a warning on Python-stdout
'''
sys.stdout.write(f'warning: {id}\n')
def JM_update_stream(doc, obj, buffer_, compress):
'''
update a stream object
compress stream when beneficial
'''
len_, _ = mupdf.fz_buffer_storage(buffer_)
nlen = len_
if len_ > 30:
nres = JM_compress_buffer(buffer_)
assert isinstance(nres, mupdf.FzBuffer)
nlen, _ = mupdf.fz_buffer_storage(nres)
if nlen < len_ and nres and compress==1:
mupdf.pdf_dict_put(
obj,
mupdf.PDF_ENUM_NAME_Filter,
mupdf.PDF_ENUM_NAME_FlateDecode,
)
mupdf.pdf_update_stream(doc, obj, nres, 1)
else:
mupdf.pdf_update_stream(doc, obj, buffer_, 0)
def JM_xobject_from_page(pdfout, fsrcpage, xref, gmap):
'''
Make an XObject from a PDF page
For a positive xref assume that its object can be used instead
'''
assert isinstance(gmap, mupdf.PdfGraftMap), f'{type(gmap)=}'
if xref > 0:
xobj1 = mupdf.pdf_new_indirect(pdfout, xref, 0)
else:
srcpage = mupdf.pdf_page_from_fz_page(fsrcpage.this)
spageref = srcpage.obj()
mediabox = mupdf.pdf_to_rect(mupdf.pdf_dict_get_inheritable(spageref, PDF_NAME('MediaBox')))
o = mupdf.pdf_dict_get_inheritable(spageref, PDF_NAME('Resources'))
if gmap.m_internal:
resources = mupdf.pdf_graft_mapped_object(gmap, o)
else:
resources = mupdf.pdf_graft_object(pdfout, o)
res = JM_read_contents(spageref)
xobj1 = mupdf.pdf_new_xobject(pdfout, mediabox, mupdf.FzMatrix(), mupdf.PdfObj(0), res)
JM_update_stream(pdfout, xobj1, res, 1)
mupdf.pdf_dict_put(xobj1, PDF_NAME('Resources'), resources)
return xobj1
def PySequence_Check(s):
return isinstance(s, (tuple, list))
def PySequence_Size(s):
return len(s)
MSG_BAD_ANNOT_TYPE = "bad annot type"
MSG_BAD_APN = "bad or missing annot AP/N"
MSG_BAD_ARG_INK_ANNOT = "arg must be seq of seq of float pairs"
MSG_BAD_ARG_POINTS = "bad seq of points"
MSG_BAD_BUFFER = "bad type: 'buffer'"
MSG_BAD_COLOR_SEQ = "bad color sequence"
MSG_BAD_DOCUMENT = "cannot open broken document"
MSG_BAD_FILETYPE = "bad filetype"
MSG_BAD_LOCATION = "bad location"
MSG_BAD_OC_CONFIG = "bad config number"
MSG_BAD_OC_LAYER = "bad layer number"
MSG_BAD_OC_REF = "bad 'oc' reference"
MSG_BAD_PAGEID = "bad page id"
MSG_BAD_PAGENO = "bad page number(s)"
MSG_BAD_PDFROOT = "PDF has no root"
MSG_BAD_RECT = "rect is infinite or empty"
MSG_BAD_TEXT = "bad type: 'text'"
MSG_BAD_XREF = "bad xref"
MSG_COLOR_COUNT_FAILED = "color count failed"
MSG_FILE_OR_BUFFER = "need font file or buffer"
MSG_FONT_FAILED = "cannot create font"
MSG_IS_NO_ANNOT = "is no annotation"
MSG_IS_NO_IMAGE = "is no image"
MSG_IS_NO_PDF = "is no PDF"
MSG_IS_NO_DICT = "object is no PDF dict"
MSG_PIX_NOALPHA = "source pixmap has no alpha"
MSG_PIXEL_OUTSIDE = "pixel(s) outside image"
JM_Exc_FileDataError = 'FileDataError'
PyExc_ValueError = 'ValueError'
def RAISEPY( msg, exc):
raise Exception( msg)
def PyUnicode_DecodeRawUnicodeEscape(s, errors='strict'):
if not s:
return ""
if isinstance(s, str):
rc = s.encode("utf8", errors=errors)
elif isinstance(s, bytes):
rc = s[:]
ret = rc.decode('raw_unicode_escape', errors=errors)
z = ret.find(chr(0))
if z >= 0:
ret = ret[:z]
return ret
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 CheckFont(page: 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: 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 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 CheckParent(o: typing.Any):
return
if not hasattr(o, "parent") or o.parent is None:
raise ValueError(f"orphaned object {type(o)=}: parent is None")
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 Exception:
return False
return q0.is_convex
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 Exception:
if 0:
exception_info()
return False
return not (r.is_empty or r.is_infinite)
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 Page__add_text_marker(self, quads, annot_type):
pdfpage = self._pdf_page()
rotation = JM_page_rotation(pdfpage)
def final():
if rotation != 0:
mupdf.pdf_dict_put_int(pdfpage.obj(), PDF_NAME('Rotate'), rotation)
try:
if rotation != 0:
mupdf.pdf_dict_put_int(pdfpage.obj(), PDF_NAME('Rotate'), 0)
annot = mupdf.pdf_create_annot(pdfpage, annot_type)
for item in quads:
q = JM_quad_from_py(item)
mupdf.pdf_add_annot_quad_point(annot, q)
mupdf.pdf_update_annot(annot)
JM_add_annot_id(annot, "A")
final()
except Exception:
if g_exceptions_verbose: exception_info()
final()
return
return Annot(annot)
def PDF_NAME(x):
assert isinstance(x, str)
return getattr(mupdf, f'PDF_ENUM_NAME_{x}')
def UpdateFontInfo(doc: 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 args_match(args, *types):
'''
Returns true if <args> matches <types>.
Each item in <types> is a type or tuple of types. Any of these types will
match an item in <args>. None will match anything in <args>.
'''
j = 0
for i in range(len(types)):
type_ = types[i]
if j >= len(args):
if isinstance(type_, tuple) and None in type_:
continue
else:
return False
if type_ is not None and not isinstance(args[j], type_):
return False
j += 1
if j != len(args):
return False
return True
def calc_image_matrix(width, height, tr, rotate, keep):
'''
# compute image insertion matrix
'''
trect = JM_rect_from_py(tr)
rot = mupdf.fz_rotate(rotate)
trw = trect.x1 - trect.x0
trh = trect.y1 - trect.y0
w = trw
h = trh
if keep:
large = max(width, height)
fw = width / large
fh = height / large
else:
fw = fh = 1
small = min(fw, fh)
if rotate != 0 and rotate != 180:
f = fw
fw = fh
fh = f
if fw < 1:
if trw / fw > trh / fh:
w = trh * small
h = trh
else:
w = trw
h = trw / small
elif fw != fh:
if trw / fw > trh / fh:
w = trh / small
h = trh
else:
w = trw
h = trw * small
else:
w = trw
h = trh
tmp = mupdf.fz_make_point(
(trect.x0 + trect.x1) / 2,
(trect.y0 + trect.y1) / 2,
)
mat = mupdf.fz_make_matrix(1, 0, 0, 1, -0.5, -0.5)
mat = mupdf.fz_concat(mat, rot)
mat = mupdf.fz_concat(mat, mupdf.fz_scale(w, h))
mat = mupdf.fz_concat(mat, mupdf.fz_translate(tmp.x, tmp.y))
return mat
def detect_super_script(line, ch):
if line.m_internal.wmode == 0 and line.m_internal.dir.x == 1 and line.m_internal.dir.y == 0:
return ch.m_internal.origin.y < line.m_internal.first_char.origin.y - ch.m_internal.size * 0.1
return 0
def dir_str(x):
ret = f'{x} {type(x)} ({len(dir(x))}):\n'
for i in dir(x):
ret += f' {i}\n'
return ret
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(">]"):
return text
if not bool(text):
return "[<>]"
if simple:
if glyphs is None:
otxt = "".join(["%02x" % ord(c) if ord(c) < 256 else "b7" for c in text])
else:
otxt = "".join(
["%02x" % glyphs[ord(c)][0] if ord(c) < 256 else "b7" for c in text]
)
return "[<" + otxt + ">]"
if ordering < 0:
otxt = "".join(["%04x" % glyphs[ord(c)][0] for c in text])
else:
otxt = "".join(["%04x" % ord(c) for c in text])
return "[<" + otxt + ">]"
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() + ">"
r = ""
for c in s:
oc = ord(c)
if oc > 255:
return make_utf16be(s)
if oc > 31 and oc < 127:
if c in ("(", ")", "\\"):
r += "\\"
r += c
continue
if oc > 127:
r += "\\%03o" % oc
continue
if oc == 8:
r += "\\b"
elif oc == 9:
r += "\\t"
elif oc == 10:
r += "\\n"
elif oc == 12:
r += "\\f"
elif oc == 13:
r += "\\r"
else:
r += "\\267"
return "(" + r + ")"
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 is not 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.
"""
import subprocess
if sys.platform == "win32":
cp = subprocess.run('where tesseract', shell=1, capture_output=1, check=0)
response = cp.stdout.strip()
if cp.returncode or not response:
print("Tesseract-OCR is not installed")
return False
dirname = os.path.dirname(response)
tessdata = os.path.join(dirname, "tessdata")
if os.path.exists(tessdata):
return tessdata
else:
print("unexpected: Tesseract-OCR has no 'tessdata' folder", file=sys.stderr)
return False
cp = subprocess.run('whereis tesseract-ocr', shell=1, capture_output=1, check=0)
response = cp.stdout.strip().split()
if cp.returncode or len(response) != 2:
print("Tesseract-OCR is not installed")
return False
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 is not None:
return tessdata
else:
print(
"unexpected: tesseract-ocr has no 'tessdata' folder",
file=sys.stderr,
)
return False
return False
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'.
"""
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 is None:
CSS = ""
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 is None:
name = fontcode
for fkey in font_keys:
font = fitz_fontdescriptors[fkey]
bold = font["bold"]
italic = font["italic"]
fbuff = font["loader"]()
archive.add(fbuff, fkey)
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)
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 jm_append_merge(dev):
'''
Append current path to list or merge into last path of the list.
(1) Append if first path, different item lists or not a 'stroke' version
of previous path
(2) If new path has the same items, merge its content into previous path
and change path["type"] to "fs".
(3) If "out" is callable, skip the previous and pass dictionary to it.
'''
assert isinstance(dev.out, list)
if callable(dev.method) or dev.method:
if dev.method is None:
assert 0
else:
resp = getattr(dev.out, dev.method)(dev.pathdict)
if not resp:
print("calling cdrawings callback function/method failed!", file=sys.stderr)
dev.pathdict = None
return
def append():
dev.out.append(dev.pathdict.copy())
dev.pathdict.clear()
assert isinstance(dev.out, list)
len_ = len(dev.out)
if len_ == 0:
return append()
thistype = dev.pathdict[ dictkey_type]
if thistype != 's':
return append()
prev = dev.out[ len_-1]
prevtype = prev[ dictkey_type]
if prevtype != 'f':
return append()
previtems = prev[ dictkey_items]
thisitems = dev.pathdict[ dictkey_items]
if previtems != thisitems:
return append()
try:
for k, v in dev.pathdict.items():
if k not in prev:
prev[k] = v
rc = 0
except Exception:
if g_exceptions_verbose: exception_info()
rc = -1
if rc == 0:
prev[ dictkey_type] = 'fs'
dev.pathdict.clear()
else:
print("could not merge stroke and fill path", file=sys.stderr)
append()
def jm_bbox_add_rect( dev, ctx, rect, code):
if not dev.layers:
dev.result.append( (code, JM_py_from_rect(rect)))
else:
dev.result.append( (code, JM_py_from_rect(rect), JM_EscapeStrFromStr( dev.layer_name)))
def jm_bbox_fill_image( dev, ctx, image, ctm, alpha, color_params):
r = mupdf.FzRect(mupdf.FzRect.Fixed_UNIT)
r = mupdf.ll_fz_transform_rect( r.internal(), ctm)
jm_bbox_add_rect( dev, ctx, r, "fill-image")
def jm_bbox_fill_image_mask( dev, ctx, image, ctm, colorspace, color, alpha, color_params):
try:
jm_bbox_add_rect( dev, ctx, mupdf.ll_fz_transform_rect(mupdf.fz_unit_rect, ctm), "fill-imgmask")
except Exception:
if g_exceptions_verbose: exception_info()
raise
def jm_bbox_fill_path( dev, ctx, path, even_odd, ctm, colorspace, color, alpha, color_params):
even_odd = True if even_odd else False
try:
jm_bbox_add_rect( dev, ctx, mupdf.ll_fz_bound_path(path, None, ctm), "fill-path")
except Exception:
if g_exceptions_verbose: exception_info()
raise
def jm_bbox_fill_shade( dev, ctx, shade, ctm, alpha, color_params):
try:
jm_bbox_add_rect( dev, ctx, mupdf.ll_fz_bound_shade( shade, ctm), "fill-shade")
except Exception:
if g_exceptions_verbose: exception_info()
raise
def jm_bbox_stroke_text( dev, ctx, text, stroke, ctm, *args):
try:
jm_bbox_add_rect( dev, ctx, mupdf.ll_fz_bound_text( text, stroke, ctm), "stroke-text")
except Exception:
if g_exceptions_verbose: exception_info()
raise
def jm_bbox_fill_text( dev, ctx, text, ctm, *args):
try:
jm_bbox_add_rect( dev, ctx, mupdf.ll_fz_bound_text( text, None, ctm), "fill-text")
except Exception:
if g_exceptions_verbose: exception_info()
raise
def jm_bbox_ignore_text( dev, ctx, text, ctm):
jm_bbox_add_rect( dev, ctx, mupdf.ll_fz_bound_text(text, None, ctm), "ignore-text")
def jm_bbox_stroke_path( dev, ctx, path, stroke, ctm, colorspace, color, alpha, color_params):
try:
jm_bbox_add_rect( dev, ctx, mupdf.ll_fz_bound_path( path, stroke, ctm), "stroke-path")
except Exception:
if g_exceptions_verbose: exception_info()
raise
def jm_checkquad(dev):
'''
Check whether the last 4 lines represent a quad.
Because of how we count, the lines are a polyline already, i.e. last point
of a line equals 1st point of next line.
So we check for a polygon (last line's end point equals start point).
If not true we return 0.
'''
items = dev.pathdict[ dictkey_items]
len_ = len(items)
f = [0] * 8
for i in range( 4):
line = items[ len_ - 4 + i]
temp = JM_point_from_py( line[1])
f[i * 2] = temp.x
f[i * 2 + 1] = temp.y
lp = JM_point_from_py( line[ 2])
if lp.x != f[0] or lp.y != f[1]:
return 0
dev.linecount = 0
q = mupdf.fz_make_quad(f[0], f[1], f[6], f[7], f[2], f[3], f[4], f[5])
rect = ('qu', JM_py_from_quad(q))
items[ len_ - 4] = rect
del items[ len_ - 3 : len_]
return 1
def jm_checkrect(dev):
'''
Check whether the last 3 path items represent a rectangle.
Returns 1 if we have modified the path, otherwise 0.
'''
dev.linecount = 0
orientation = 0
items = dev.pathdict[ dictkey_items]
len_ = len(items)
line0 = items[ len_ - 3]
ll = JM_point_from_py( line0[ 1])
lr = JM_point_from_py( line0[ 2])
line2 = items[ len_ - 1]
ur = JM_point_from_py( line2[ 1])
ul = JM_point_from_py( line2[ 2])
if (0
or ll.y != lr.y
or ll.x != ul.x
or ur.y != ul.y
or ur.x != lr.x
):
return 0
if ul.y < lr.y:
r = mupdf.fz_make_rect(ul.x, ul.y, lr.x, lr.y)
orientation = 1
else:
r = mupdf.fz_make_rect(ll.x, ll.y, ur.x, ur.y)
orientation = -1
rect = ( 're', JM_py_from_rect(r), orientation)
items[ len_ - 3] = rect
del items[ len_ - 2 : len_]
return 1
def jm_trace_text( dev, text, type_, ctm, colorspace, color, alpha, seqno):
span = text.head
while 1:
if not span:
break
jm_trace_text_span( dev, span, type_, ctm, colorspace, color, alpha, seqno)
span = span.next
def jm_trace_text_span(dev, span, type_, ctm, colorspace, color, alpha, seqno):
'''
jm_trace_text_span(fz_context *ctx, PyObject *out, fz_text_span *span, int type, fz_matrix ctm, fz_colorspace *colorspace, const float *color, float alpha, size_t seqno)
'''
out_font = None
assert isinstance( span, mupdf.fz_text_span)
span = mupdf.FzTextSpan( span)
assert isinstance( ctm, mupdf.fz_matrix)
ctm = mupdf.FzMatrix( ctm)
fontname = JM_font_name( span.font())
mat = mupdf.fz_concat(span.trm(), ctm)
dir = mupdf.fz_transform_vector(mupdf.fz_make_point(1, 0), mat)
fsize = math.sqrt(dir.x * dir.x + dir.y * dir.y)
dir = mupdf.fz_normalize_vector(dir)
space_adv = 0
asc = JM_font_ascender( span.font())
dsc = JM_font_descender( span.font())
if asc < 1e-3:
dsc = -0.1
asc = 0.9
ascsize = asc * fsize / (asc - dsc)
dscsize = dsc * fsize / (asc - dsc)
fflags = 0
mono = mupdf.fz_font_is_monospaced( span.font())
fflags += mono * TEXT_FONT_MONOSPACED
fflags += mupdf.fz_font_is_italic( span.font()) * TEXT_FONT_ITALIC
fflags += mupdf.fz_font_is_serif( span.font()) * TEXT_FONT_SERIFED
fflags += mupdf.fz_font_is_bold( span.font()) * TEXT_FONT_BOLD
last_adv = 0
span_bbox = mupdf.FzRect()
rot = mupdf.fz_make_matrix(dir.x, dir.y, -dir.y, dir.x, 0, 0)
if dir.x == -1:
rot.d = 1
chars = []
for i in range( span.m_internal.len):
adv = 0
if span.items(i).gid >= 0:
adv = mupdf.fz_advance_glyph( span.font(), span.items(i).gid, span.m_internal.wmode)
adv *= fsize
last_adv = adv
if span.items(i).ucs == 32:
space_adv = adv
char_orig = mupdf.fz_make_point(span.items(i).x, span.items(i).y)
char_orig = mupdf.fz_transform_point(char_orig, ctm)
m1 = mupdf.fz_make_matrix(1, 0, 0, 1, -char_orig.x, -char_orig.y)
m1 = mupdf.fz_concat(m1, rot)
m1 = mupdf.fz_concat(m1, mupdf.FzMatrix(1, 0, 0, 1, char_orig.x, char_orig.y))
x0 = char_orig.x
x1 = x0 + adv
if (
(mat.d > 0 and (dir.x == 1 or dir.x == -1))
or
(mat.b != 0 and mat.b == -mat.c)
):
y0 = char_orig.y + dscsize
y1 = char_orig.y + ascsize
else:
y0 = char_orig.y - ascsize
y1 = char_orig.y - dscsize
char_bbox = mupdf.fz_make_rect(x0, y0, x1, y1)
char_bbox = mupdf.fz_transform_rect(char_bbox, m1)
chars.append(
(
span.items(i).ucs,
span.items(i).gid,
(
char_orig.x,
char_orig.y,
),
(
char_bbox.x0,
char_bbox.y0,
char_bbox.x1,
char_bbox.y1,
),
)
)
if i > 0:
span_bbox = mupdf.fz_union_rect(span_bbox, char_bbox)
else:
span_bbox = char_bbox
chars = tuple(chars)
if not space_adv:
if not mono:
c, out_font = mupdf.fz_encode_character_with_fallback( span.font(), 32, 0, 0)
space_adv = mupdf.fz_advance_glyph(
span.font(),
c,
span.m_internal.wmode,
)
space_adv *= fsize
if not space_adv:
space_adv = last_adv
else:
space_adv = last_adv
span_dict = dict()
span_dict[ 'dir'] = JM_py_from_point(dir)
span_dict[ 'font'] = JM_EscapeStrFromStr(fontname)
span_dict[ 'wmode'] = span.m_internal.wmode
span_dict[ 'flags'] =fflags
span_dict[ "bidi_lvl"] =span.m_internal.bidi_level
span_dict[ "bidi_dir"] = span.m_internal.markup_dir
span_dict[ 'ascender'] = asc
span_dict[ 'descender'] = dsc
span_dict[ 'colorspace'] = 3
if colorspace:
rgb = mupdf.fz_convert_color(
mupdf.FzColorspace( mupdf.ll_fz_keep_colorspace( colorspace)),
color,
mupdf.fz_device_rgb(),
mupdf.FzColorspace(),
mupdf.FzColorParams(),
)
rgb = rgb[:3]
else:
rgb = (0, 0, 0)
if dev.linewidth > 0:
linewidth = dev.linewidth
else:
linewidth = fsize * 0.05
span_dict[ 'color'] = rgb
span_dict[ 'size'] = fsize
span_dict[ "opacity"] = alpha
span_dict[ "linewidth"] = linewidth
span_dict[ "spacewidth"] = space_adv
span_dict[ 'type'] = type_
span_dict[ 'bbox'] = JM_py_from_rect(span_bbox)
span_dict[ 'layer'] = JM_EscapeStrFromStr( dev.layer_name)
span_dict[ "seqno"] = seqno
span_dict[ 'chars'] = chars
dev.out.append( span_dict)
def jm_lineart_color(colorspace, color):
if colorspace:
try:
cs = mupdf.FzColorspace( mupdf.FzColorspace.Fixed_RGB)
cp = mupdf.FzColorParams()
rgb = mupdf.ll_fz_convert_color(
colorspace,
color,
cs.m_internal,
None,
cp.internal(),
)
except Exception:
if g_exceptions_verbose: exception_info()
raise
return rgb[:3]
return ()
def jm_lineart_drop_device(dev, ctx):
if isinstance(dev.out, list):
dev.out = []
dev.scissors = []
def jm_lineart_fill_path( dev, ctx, path, even_odd, ctm, colorspace, color, alpha, color_params):
even_odd = True if even_odd else False
try:
assert isinstance( ctm, mupdf.fz_matrix)
dev.ctm = mupdf.FzMatrix( ctm)
dev.path_type = trace_device_FILL_PATH
jm_lineart_path( dev, ctx, path)
if dev.pathdict is None:
return
dev.pathdict[ dictkey_type] ="f"
dev.pathdict[ "even_odd"] = even_odd
dev.pathdict[ "fill_opacity"] = alpha
dev.pathdict[ "fill"] = jm_lineart_color( colorspace, color)
dev.pathdict[ dictkey_rect] = JM_py_from_rect(dev.pathrect)
dev.pathdict[ "seqno"] = dev.seqno
dev.pathdict[ 'layer'] = JM_EscapeStrFromStr( dev.layer_name)
if dev.clips:
dev.pathdict[ 'level'] = dev.depth
jm_append_merge(dev)
dev.seqno += 1
except Exception:
if g_exceptions_verbose: exception_info()
raise
def jm_lineart_fill_text( dev, ctx, text, ctm, colorspace, color, alpha, color_params):
if 0:
log(f'{type(ctx)=} {ctx=}')
log(f'{type(dev)=} {dev=}')
log(f'{type(text)=} {text=}')
log(f'{type(ctm)=} {ctm=}')
log(f'{type(colorspace)=} {colorspace=}')
log(f'{type(color)=} {color=}')
log(f'{type(alpha)=} {alpha=}')
log(f'{type(color_params)=} {color_params=}')
jm_trace_text(dev, text, 0, ctm, colorspace, color, alpha, dev.seqno)
dev.seqno += 1
def jm_lineart_ignore_text(dev, text, ctm):
jm_trace_text(dev, text, 3, ctm, None, None, 1, dev.seqno)
dev.seqno += 1
class Walker(mupdf.FzPathWalker2):
def __init__(self, dev):
super().__init__()
self.use_virtual_moveto()
self.use_virtual_lineto()
self.use_virtual_curveto()
self.use_virtual_closepath()
self.dev = dev
def closepath(self, ctx):
try:
if self.dev.linecount == 3:
if jm_checkrect(self.dev):
return
self.dev.pathdict[ "closePath"] = True
self.dev.linecount = 0
except Exception:
if g_exceptions_verbose: exception_info()
raise
def curveto(self, ctx, x1, y1, x2, y2, x3, y3):
try:
self.dev.linecount = 0
p1 = mupdf.fz_make_point(x1, y1)
p2 = mupdf.fz_make_point(x2, y2)
p3 = mupdf.fz_make_point(x3, y3)
p1 = mupdf.fz_transform_point(p1, self.dev.ctm)
p2 = mupdf.fz_transform_point(p2, self.dev.ctm)
p3 = mupdf.fz_transform_point(p3, self.dev.ctm)
self.dev.pathrect = mupdf.fz_include_point_in_rect(self.dev.pathrect, p1)
self.dev.pathrect = mupdf.fz_include_point_in_rect(self.dev.pathrect, p2)
self.dev.pathrect = mupdf.fz_include_point_in_rect(self.dev.pathrect, p3)
list_ = (
"c",
JM_py_from_point(self.dev.lastpoint),
JM_py_from_point(p1),
JM_py_from_point(p2),
JM_py_from_point(p3),
)
self.dev.lastpoint = p3
self.dev.pathdict[ dictkey_items].append( list_)
except Exception:
if g_exceptions_verbose: exception_info()
raise
def lineto(self, ctx, x, y):
try:
p1 = mupdf.fz_transform_point( mupdf.fz_make_point(x, y), self.dev.ctm)
self.dev.pathrect = mupdf.fz_include_point_in_rect( self.dev.pathrect, p1)
list_ = (
'l',
JM_py_from_point( self.dev.lastpoint),
JM_py_from_point(p1),
)
self.dev.lastpoint = p1
items = self.dev.pathdict[ dictkey_items]
items.append( list_)
self.dev.linecount += 1
if self.dev.linecount == 4 and self.dev.path_type != trace_device_FILL_PATH:
jm_checkquad(self.dev)
except Exception:
if g_exceptions_verbose: exception_info()
raise
def moveto(self, ctx, x, y):
if 0 and isinstance(self.dev.pathdict, dict):
log(f'self.dev.pathdict:')
for n, v in self.dev.pathdict.items():
log( ' {type(n)=} {len(n)=} {n!r} {n}: {v!r}: {v}')
try:
self.dev.lastpoint = mupdf.fz_transform_point(
mupdf.fz_make_point(x, y),
self.dev.ctm,
)
if mupdf.fz_is_infinite_rect( self.dev.pathrect):
self.dev.pathrect = mupdf.fz_make_rect(
self.dev.lastpoint.x,
self.dev.lastpoint.y,
self.dev.lastpoint.x,
self.dev.lastpoint.y,
)
self.dev.linecount = 0
except Exception:
if g_exceptions_verbose: exception_info()
raise
def jm_lineart_path(dev, ctx, path):
'''
Create the "items" list of the path dictionary
* either create or empty the path dictionary
* reset the end point of the path
* reset count of consecutive lines
* invoke fz_walk_path(), which create the single items
* if no items detected, empty path dict again
'''
try:
dev.pathrect = mupdf.FzRect( mupdf.FzRect.Fixed_INFINITE)
dev.linecount = 0
dev.lastpoint = mupdf.FzPoint( 0, 0)
dev.pathdict = dict()
dev.pathdict[ dictkey_items] = []
walker = Walker(dev)
mupdf.fz_walk_path( mupdf.FzPath(mupdf.ll_fz_keep_path(path)), walker, walker.m_internal)
if not dev.pathdict[ dictkey_items]:
dev.pathdict = None
except Exception:
if g_exceptions_verbose: exception_info()
raise
def jm_lineart_stroke_path( dev, ctx, path, stroke, ctm, colorspace, color, alpha, color_params):
try:
assert isinstance( ctm, mupdf.fz_matrix)
dev.pathfactor = 1
if abs(ctm.a) == abs(ctm.d):
dev.pathfactor = abs(ctm.a)
dev.ctm = mupdf.FzMatrix( ctm)
dev.path_type = trace_device_STROKE_PATH
jm_lineart_path( dev, ctx, path)
if dev.pathdict is None:
return
dev.pathdict[ dictkey_type] = 's'
dev.pathdict[ 'stroke_opacity'] = alpha
dev.pathdict[ 'color'] = jm_lineart_color( colorspace, color)
dev.pathdict[ dictkey_width] = dev.pathfactor * stroke.linewidth
dev.pathdict[ 'lineCap'] = (
stroke.start_cap,
stroke.dash_cap,
stroke.end_cap,
)
dev.pathdict[ 'lineJoin'] = dev.pathfactor * stroke.linejoin
if 'closePath' not in dev.pathdict:
dev.pathdict['closePath'] = False
if stroke.dash_len:
buff = mupdf.fz_new_buffer( 256)
mupdf.fz_append_string( buff, "[ ")
for i in range( stroke.dash_len):
value = mupdf.floats_getitem( stroke.dash_list, i)
mupdf.fz_append_string( buff, f'{dev.pathfactor * value:g} ')
mupdf.fz_append_string( buff, f'] {dev.pathfactor * stroke.dash_phase:g}')
dev.pathdict[ 'dashes'] = buff
else:
dev.pathdict[ 'dashes'] = '[] 0'
dev.pathdict[ dictkey_rect] = JM_py_from_rect(dev.pathrect)
dev.pathdict['layer'] = JM_EscapeStrFromStr( dev.layer_name)
dev.pathdict[ 'seqno'] = dev.seqno
if dev.clips:
dev.pathdict[ 'level'] = dev.depth
jm_append_merge(dev)
dev.seqno += 1
except Exception:
if g_exceptions_verbose: exception_info()
raise
def jm_lineart_clip_path(dev, ctx, path, even_odd, ctm, scissor):
if not dev.clips:
return
dev.ctm = mupdf.FzMatrix(ctm)
dev.path_type = trace_device_CLIP_PATH
jm_lineart_path(dev, ctx, path)
if dev.pathdict is None:
return
dev.pathdict[ dictkey_type] = 'clip'
dev.pathdict[ 'even_odd'] = bool(even_odd)
if 'closePath' not in dev.pathdict:
dev.pathdict['closePath'] = False
dev.pathdict['scissor'] = JM_py_from_rect(compute_scissor(dev))
dev.pathdict['level'] = dev.depth
dev.pathdict['layer'] = JM_EscapeStrFromStr( dev.layer_name)
jm_append_merge(dev)
dev.depth += 1
def jm_lineart_clip_stroke_path(dev, ctx, path, stroke, ctm, scissor):
if not dev.clips:
return
dev.ctm = mupdf.FzMatrix(ctm)
dev.path_type = trace_device_CLIP_STROKE_PATH
jm_lineart_path(dev, ctx, path)
if dev.pathdict is None:
return
dev.pathdict['dictkey_type'] = 'clip'
dev.pathdict['even_odd'] = None
if 'closePath' not in dev.pathdict:
dev.pathdict['closePath'] = False
dev.pathdict['scissor'] = JM_py_from_rect(compute_scissor(dev))
dev.pathdict['level'] = dev.depth
dev.pathdict['layer'] = JM_EscapeStrFromStr( dev.layer_name)
jm_append_merge(dev)
dev.depth += 1
def jm_lineart_clip_stroke_text(dev, ctx, text, stroke, ctm, scissor):
if not dev.clips:
return
compute_scissor(dev)
dev.depth += 1
def jm_lineart_clip_text(dev, ctx, text, ctm, scissor):
if not dev.clips:
return
compute_scissor(dev)
dev.depth += 1
def jm_lineart_clip_image_mask( dev, ctx, image, ctm, scissor):
if not dev.clips:
return
compute_scissor(dev)
dev.depth += 1
def jm_lineart_pop_clip(dev, ctx):
if not dev.clips or not dev.scissors:
return
len_ = len(dev.scissors)
if len_ < 1:
return
del dev.scissors[-1]
dev.depth -= 1
def jm_lineart_begin_layer(dev, ctx, name):
dev.layer_name = name
def jm_lineart_end_layer(dev, ctx):
dev.layer_name = None
def jm_lineart_begin_group(dev, ctx, bbox, cs, isolated, knockout, blendmode, alpha):
if not dev.clips:
return
dev.pathdict = {
"type": "group",
"rect": JM_py_from_rect(bbox),
"isolated": bool(isolated),
"knockout": bool(knockout),
"blendmode": mupdf.fz_blendmode_name(blendmode),
"opacity": alpha,
"level": dev.depth,
"layer": JM_EscapeStrFromStr( dev.layer_name),
}
jm_append_merge(dev)
dev.depth += 1
def jm_lineart_end_group(dev, ctx):
if not dev.clips:
return
dev.depth -= 1
def jm_lineart_stroke_text(dev, ctx, text, stroke, ctm, colorspace, color, alpha, color_params):
jm_trace_text(dev, text, 1, ctm, colorspace, color, alpha, dev.seqno)
dev.seqno += 1
def jm_dev_linewidth( dev, ctx, path, stroke, matrix, colorspace, color, alpha, color_params):
dev.linewidth = stroke.linewidth
jm_increase_seqno( dev, ctx)
def jm_increase_seqno( dev, ctx, *vargs):
try:
dev.seqno += 1
except Exception:
if g_exceptions_verbose: exception_info()
raise
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))
class JM_image_reporter_Filter(mupdf.PdfFilterOptions2):
def __init__(self):
super().__init__()
self.use_virtual_image_filter()
def image_filter( self, ctx, ctm, name, image):
assert isinstance(ctm, mupdf.fz_matrix)
JM_image_filter(self, mupdf.FzMatrix(ctm), name, image)
if mupdf_cppyy:
return 0
class JM_new_bbox_device_Device(mupdf.FzDevice2):
def __init__(self, result, layers):
super().__init__()
self.result = result
self.layers = layers
self.use_virtual_fill_path()
self.use_virtual_stroke_path()
self.use_virtual_fill_text()
self.use_virtual_stroke_text()
self.use_virtual_ignore_text()
self.use_virtual_fill_shade()
self.use_virtual_fill_image()
self.use_virtual_fill_image_mask()
self.use_virtual_begin_layer()
self.use_virtual_end_layer()
self.begin_layer = jm_lineart_begin_layer
self.end_layer = jm_lineart_end_layer
fill_path = jm_bbox_fill_path
stroke_path = jm_bbox_stroke_path
fill_text = jm_bbox_fill_text
stroke_text = jm_bbox_stroke_text
ignore_text = jm_bbox_ignore_text
fill_shade = jm_bbox_fill_shade
fill_image = jm_bbox_fill_image
fill_image_mask = jm_bbox_fill_image_mask
class JM_new_output_fileptr_Output(mupdf.FzOutput2):
def __init__(self, bio):
super().__init__()
self.bio = bio
self.use_virtual_write()
self.use_virtual_seek()
self.use_virtual_tell()
self.use_virtual_truncate()
def seek( self, ctx, *args, **kwargs):
return self.bio.seek( *args, **kwargs)
def tell( self, ctx, *args, **kwargs):
ret = self.bio.tell( *args, **kwargs)
return ret
def truncate( self, ctx, *args, **kwargs):
return self.bio.truncate( *args, **kwargs)
def write(self, ctx, data_raw, data_length):
data = mupdf.raw_to_python_bytes(data_raw, data_length)
return self.bio.write(data)
def compute_scissor(dev):
'''
Every scissor of a clip is a sub rectangle of the preceeding clip scissor
if the clip level is larger.
'''
if dev.scissors is None:
dev.scissors = list()
num_scissors = len(dev.scissors)
if num_scissors > 0:
last_scissor = dev.scissors[num_scissors-1]
scissor = JM_rect_from_py(last_scissor)
scissor = mupdf.fz_intersect_rect(scissor, dev.pathrect)
else:
scissor = dev.pathrect
dev.scissors.append(JM_py_from_rect(scissor))
return scissor
class JM_new_lineart_device_Device(mupdf.FzDevice2):
'''
LINEART device for Python method Page.get_cdrawings()
'''
def __init__(self, out, clips, method):
super().__init__()
self.use_virtual_fill_path()
self.use_virtual_stroke_path()
self.use_virtual_clip_path()
self.use_virtual_clip_image_mask()
self.use_virtual_clip_stroke_path()
self.use_virtual_clip_stroke_text()
self.use_virtual_clip_text()
self.use_virtual_fill_text
self.use_virtual_stroke_text
self.use_virtual_ignore_text
self.use_virtual_fill_shade()
self.use_virtual_fill_image()
self.use_virtual_fill_image_mask()
self.use_virtual_pop_clip()
self.use_virtual_begin_group()
self.use_virtual_end_group()
self.use_virtual_begin_layer()
self.use_virtual_end_layer()
self.out = out
self.seqno = 0
self.depth = 0
self.clips = clips
self.method = method
self.scissors = None
self.layer_name = None
self.pathrect = None
self.linewidth = 0
self.ptm = mupdf.FzMatrix()
self.ctm = mupdf.FzMatrix()
self.rot = mupdf.FzMatrix()
self.lastpoint = mupdf.FzPoint()
self.pathrect = mupdf.FzRect()
self.pathfactor = 0
self.linecount = 0
self.path_type = 0
fill_path = jm_lineart_fill_path
stroke_path = jm_lineart_stroke_path
clip_image_mask = jm_lineart_clip_image_mask
clip_path = jm_lineart_clip_path
clip_stroke_path = jm_lineart_clip_stroke_path
clip_text = jm_lineart_clip_text
clip_stroke_text = jm_lineart_clip_stroke_text
fill_text = jm_increase_seqno
stroke_text = jm_increase_seqno
ignore_text = jm_increase_seqno
fill_shade = jm_increase_seqno
fill_image = jm_increase_seqno
fill_image_mask = jm_increase_seqno
pop_clip = jm_lineart_pop_clip
begin_group = jm_lineart_begin_group
end_group = jm_lineart_end_group
begin_layer = jm_lineart_begin_layer
end_layer = jm_lineart_end_layer
class JM_new_texttrace_device(mupdf.FzDevice2):
'''
Trace TEXT device for Python method Page.get_texttrace()
'''
def __init__(self, out):
super().__init__()
self.use_virtual_fill_path()
self.use_virtual_stroke_path()
self.use_virtual_fill_text()
self.use_virtual_stroke_text()
self.use_virtual_ignore_text()
self.use_virtual_fill_shade()
self.use_virtual_fill_image()
self.use_virtual_fill_image_mask()
self.use_virtual_begin_layer()
self.use_virtual_end_layer()
self.out = out
self.seqno = 0
self.depth = 0
self.clips = 0
self.method = None
self.seqno = 0
self.pathdict = dict()
self.scissors = list()
self.linewidth = 0
self.ptm = mupdf.FzMatrix()
self.ctm = mupdf.FzMatrix()
self.rot = mupdf.FzMatrix()
self.lastpoint = mupdf.FzPoint()
self.pathrect = mupdf.FzRect()
self.pathfactor = 0
self.linecount = 0
self.path_type = 0
self.layer_name = None
fill_path = jm_increase_seqno
stroke_path = jm_dev_linewidth
fill_text = jm_lineart_fill_text
stroke_text = jm_lineart_stroke_text
ignore_text = jm_lineart_ignore_text
fill_shade = jm_increase_seqno
fill_image = jm_increase_seqno
fill_image_mask = jm_increase_seqno
begin_layer = jm_lineart_begin_layer
end_layer = jm_lineart_end_layer
def _get_glyph_text() -> bytes:
'''
Adobe Glyph List function
'''
import base64
import gzip
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 ConversionHeader(i: str, filename: OptStr ="unknown"):
t = i.lower()
import textwrap
html = textwrap.dedent("""
<!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>
""")
xml = textwrap.dedent("""
<?xml version="1.0"?>
<document name="%s">
"""
% filename
)
xhtml = textwrap.dedent("""
<?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>
""")
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
def adobe_glyph_names() -> tuple:
'''
Adobe Glyph List function
'''
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:
'''
Adobe Glyph List function
'''
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 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.
"""
assert isinstance( page, Page)
assert isinstance( annot, Annot)
annot.parent = page
page._annot_refs[id(annot)] = annot
annot.thisown = True
def canon(c):
assert isinstance(c, int)
if c == 0xA0 or c == 0x2028 or c == 0x2029:
return ord(' ')
if c == ord('\r') or c == ord('\n') or c == ord('\t'):
return ord(' ')
if c >= ord('A') and c <= ord('Z'):
return c - ord('A') + ord('a')
return c
def chartocanon(s):
assert isinstance(s, str)
n, c = mupdf.fz_chartorune(s)
c = canon(c)
return n, c
def dest_is_valid(o, page_count, page_object_nums, names_list):
p = mupdf.pdf_dict_get( o, PDF_NAME('A'))
if (
mupdf.pdf_name_eq(
mupdf.pdf_dict_get( p, PDF_NAME('S')),
PDF_NAME('GoTo')
)
and not string_in_names_list(
mupdf.pdf_dict_get( p, PDF_NAME('D')),
names_list
)
):
return 0
p = mupdf.pdf_dict_get( o, PDF_NAME('Dest'))
if not p.m_internal:
pass
elif mupdf.pdf_is_string( p):
return string_in_names_list( p, names_list)
elif not dest_is_valid_page(
mupdf.pdf_array_get( p, 0),
page_object_nums,
page_count,
):
return 0
return 1
def dest_is_valid_page(obj, page_object_nums, pagecount):
num = mupdf.pdf_to_num(obj)
if num == 0:
return 0
for i in range(pagecount):
if page_object_nums[i] == num:
return 1
return 0
def find_string(s, needle):
assert isinstance(s, str)
for i in range(len(s)):
end = match_string(s[i:], needle)
if end is not None:
end += i
return i, end
return None, None
def get_pdf_now() -> str:
'''
"Now" timestamp in PDF Format
'''
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
class ElementPosition(object):
"""Convert a dictionary with element position information to an object."""
def __init__(self):
pass
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.
"""
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 []
blocks = page.get_text(
"dict", flags=0, clip=clip,
)["blocks"]
lines = []
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 == []:
return lines
lines.sort(key=lambda bbox: bbox.y1)
bboxf = lines.pop(0)
if bboxf.y0 - start.y <= 0.1 * bboxf.height:
r = Rect(start.x, bboxf.y0, bboxf.br)
if not (r.is_empty or r.is_infinite):
lines.insert(0, r)
else:
lines.insert(0, bboxf)
if lines == []:
return lines
bboxl = lines.pop()
if stop.y - bboxl.y1 <= 0.1 * bboxl.height:
r = Rect(bboxl.tl, stop.x, bboxl.y1)
if not (r.is_empty or r.is_infinite):
lines.append(r)
else:
lines.append(bboxl)
return lines
def glyph_name_to_unicode(name: str) -> int:
'''
Adobe Glyph List function
'''
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 hdist(dir, a, b):
dx = b.x - a.x
dy = b.y - a.y
return mupdf.fz_abs(dx * dir.x + dy * dir.y)
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)
if rect.is_empty or rect.is_infinite:
raise ValueError("rect must be finite and not empty")
tl = rect.tl
height = rect.height / rows
width = rect.width / cols
delta_h = (width, 0, width, 0)
delta_v = (0, height, 0, height)
r = Rect(tl, tl.x + width, tl.y + height)
row = [r]
for i in range(1, cols):
r += delta_h
row.append(r)
rects = [row]
for i in range(1, rows):
row = rects[i - 1]
nrow = []
for r in row:
nrow.append(r + delta_v)
rects.append(nrow)
return rects
def util_ensure_widget_calc(annot):
'''
Ensure that widgets with /AA/C JavaScript are in array AcroForm/CO
'''
annot_obj = mupdf.pdf_annot_obj(annot.this)
pdf = mupdf.pdf_get_bound_document(annot_obj)
PDFNAME_CO = mupdf.pdf_new_name("CO")
acro = mupdf.pdf_dict_getl(
mupdf.pdf_trailer(pdf),
PDF_NAME('Root'),
PDF_NAME('AcroForm'),
)
CO = mupdf.pdf_dict_get(acro, PDFNAME_CO)
if not CO.this:
CO = mupdf.pdf_dict_put_array(acro, PDFNAME_CO, 2)
n = mupdf.pdf_array_len(CO)
found = 0
xref = mupdf.pdf_to_num(annot_obj)
for i in range(n):
nxref = mupdf.pdf_to_num(mupdf.pdf_array_get(CO, i))
if xref == nxref:
found = 1
break
if not found:
mupdf.pdf_array_push(CO, mupdf.pdf_new_indirect(pdf, xref, 0))
def util_make_rect( *args, p0=None, p1=None, x0=None, y0=None, x1=None, y1=None):
'''
Helper for initialising rectangle classes.
2022-09-02: This is quite different from PyMuPDF's util_make_rect(), which
uses `goto` in ways that don't easily translate to Python.
Returns (x0, y0, x1, y1) derived from <args>, then override with p0, p1,
x0, y0, x1, y1 if they are not None.
Accepts following forms for <args>:
() returns all zeros.
(top-left, bottom-right)
(top-left, x1, y1)
(x0, y0, bottom-right)
(x0, y0, x1, y1)
(rect)
Where top-left and bottom-right are (x, y) or something with .x, .y
members; rect is something with .x0, .y0, .x1, and .y1 members.
2023-11-18: we now override with p0, p1, x0, y0, x1, y1 if not None.
'''
def get_xy( arg):
if isinstance( arg, (list, tuple)) and len( arg) == 2:
return arg[0], arg[1]
if isinstance( arg, (Point, mupdf.FzPoint, mupdf.fz_point)):
return arg.x, arg.y
return None, None
def make_tuple( a):
if isinstance( a, tuple):
return a
if isinstance( a, Point):
return a.x, a.y
elif isinstance( a, (Rect, IRect, mupdf.FzRect, mupdf.fz_rect)):
return a.x0, a.y0, a.x1, a.y1
if not isinstance( a, (list, tuple)):
a = a,
return a
def handle_args():
if len(args) == 0:
return 0, 0, 0, 0
elif len(args) == 1:
arg = args[0]
if isinstance( arg, (list, tuple)) and len( arg) == 2:
p1, p2 = arg
return *p1, *p2
if isinstance( arg, (list, tuple)) and len( arg) == 3:
a, b, c = arg
a = make_tuple(a)
b = make_tuple(b)
c = make_tuple(c)
ret = *a, *b, *c
return ret
arg = make_tuple( arg)
return arg
elif len(args) == 2:
return get_xy( args[0]) + get_xy( args[1])
elif len(args) == 3:
x0, y0 = get_xy( args[0])
if (x0, y0) != (None, None):
return x0, y0, args[1], args[2]
x1, y1 = get_xy( args[2])
if (x1, y1) != (None, None):
return args[0], args[1], x1, y1
elif len(args) == 4:
return args[0], args[1], args[2], args[3]
raise Exception( f'Unrecognised args: {args}')
ret_x0, ret_y0, ret_x1, ret_y1 = handle_args()
if p0 is not None: ret_x0, ret_y0 = get_xy(p0)
if p1 is not None: ret_x1, ret_y1 = get_xy(p1)
if x0 is not None: ret_x0 = x0
if y0 is not None: ret_y0 = y0
if x1 is not None: ret_x1 = x1
if y1 is not None: ret_y1 = y1
return ret_x0, ret_y0, ret_x1, ret_y1
def util_make_irect( *args, p0=None, p1=None, x0=None, y0=None, x1=None, y1=None):
a, b, c, d = util_make_rect( *args, p0=p0, p1=p1, x0=x0, y0=y0, x1=x1, y1=y1)
def convert(x):
ret = int(x)
assert ret == x
return ret
a = convert(a)
b = convert(b)
c = convert(c)
d = convert(d)
return a, b, c, d
def util_round_rect( rect):
return JM_py_from_irect(mupdf.fz_round_rect(JM_rect_from_py(rect)))
def util_transform_rect( rect, matrix):
if g_use_extra:
return extra.util_transform_rect( rect, matrix)
return JM_py_from_rect(mupdf.fz_transform_rect(JM_rect_from_py(rect), JM_matrix_from_py(matrix)))
def util_intersect_rect( r1, r2):
return JM_py_from_rect(
mupdf.fz_intersect_rect(
JM_rect_from_py(r1),
JM_rect_from_py(r2),
)
)
def util_is_point_in_rect( p, r):
return mupdf.fz_is_point_inside_rect(
JM_point_from_py(p),
JM_rect_from_py(r),
)
def util_include_point_in_rect( r, p):
return JM_py_from_rect(
mupdf.fz_include_point_in_rect(
JM_rect_from_py(r),
JM_point_from_py(p),
)
)
def util_point_in_quad( P, Q):
p = JM_point_from_py(P)
q = JM_quad_from_py(Q)
return mupdf.fz_is_point_inside_quad(p, q)
def util_transform_point( point, matrix):
return JM_py_from_point(
mupdf.fz_transform_point(
JM_point_from_py(point),
JM_matrix_from_py(matrix),
)
)
def util_union_rect( r1, r2):
return JM_py_from_rect(
mupdf.fz_union_rect(
JM_rect_from_py(r1),
JM_rect_from_py(r2),
)
)
def util_concat_matrix( m1, m2):
return JM_py_from_matrix(
mupdf.fz_concat(
JM_matrix_from_py(m1),
JM_matrix_from_py(m2),
)
)
def util_invert_matrix(matrix):
if 0:
if isinstance( matrix, (tuple, list)):
matrix = mupdf.FzMatrix( *matrix)
elif isinstance( matrix, mupdf.fz_matrix):
matrix = mupdf.FzMatrix( matrix)
elif isinstance( matrix, Matrix):
matrix = mupdf.FzMatrix( matrix.a, matrix.b, matrix.c, matrix.d, matrix.e, matrix.f)
assert isinstance( matrix, mupdf.FzMatrix), f'{type(matrix)=}: {matrix}'
ret = mupdf.fz_invert_matrix( matrix)
if ret == matrix and (0
or abs( matrix.a - 1) >= sys.float_info.epsilon
or abs( matrix.b - 0) >= sys.float_info.epsilon
or abs( matrix.c - 0) >= sys.float_info.epsilon
or abs( matrix.d - 1) >= sys.float_info.epsilon
):
return 1, ()
return 0, (ret.a, ret.b, ret.c, ret.d, ret.e, ret.f)
src = JM_matrix_from_py(matrix)
a = src.a
det = a * src.d - src.b * src.c
if det < -sys.float_info.epsilon or det > sys.float_info.epsilon:
dst = mupdf.FzMatrix()
rdet = 1 / det
dst.a = src.d * rdet
dst.b = -src.b * rdet
dst.c = -src.c * rdet
dst.d = a * rdet
a = -src.e * dst.a - src.f * dst.c
dst.f = -src.e * dst.b - src.f * dst.d
dst.e = a
return 0, (dst.a, dst.b, dst.c, dst.d, dst.e, dst.f)
return 1, ()
def util_measure_string( text, fontname, fontsize, encoding):
font = mupdf.fz_new_base14_font(fontname)
w = 0
pos = 0
while pos < len(text):
t, c = mupdf.fz_chartorune(text[pos:])
pos += t
if encoding == mupdf.PDF_SIMPLE_ENCODING_GREEK:
c = mupdf.fz_iso8859_7_from_unicode(c)
elif encoding == mupdf.PDF_SIMPLE_ENCODING_CYRILLIC:
c = mupdf.fz_windows_1251_from_unicode(c)
else:
c = mupdf.fz_windows_1252_from_unicode(c)
if c < 0:
c = 0xB7
g = mupdf.fz_encode_character(font, c)
dw = mupdf.fz_advance_glyph(font, g, 0)
w += dw
ret = w * fontsize
return ret
def util_sine_between(C, P, Q):
c = JM_point_from_py(C)
p = JM_point_from_py(P)
q = JM_point_from_py(Q)
s = mupdf.fz_normalize_vector(mupdf.fz_make_point(q.x - p.x, q.y - p.y))
m1 = mupdf.fz_make_matrix(1, 0, 0, 1, -p.x, -p.y)
m2 = mupdf.fz_make_matrix(s.x, -s.y, s.y, s.x, 0, 0)
m1 = mupdf.fz_concat(m1, m2)
c = mupdf.fz_transform_point(c, m1)
c = mupdf.fz_normalize_vector(c)
return c.y
def util_hor_matrix(C, P):
'''
Return the matrix that maps two points C, P to the x-axis such that
C -> (0,0) and the image of P have the same distance.
'''
c = JM_point_from_py(C)
p = JM_point_from_py(P)
s = mupdf.fz_normalize_vector(mupdf.fz_make_point(p.x - c.x, p.y - c.y))
m1 = mupdf.fz_make_matrix(1, 0, 0, 1, -c.x, -c.y)
m2 = mupdf.fz_make_matrix(s.x, -s.y, s.y, s.x, 0, 0)
return JM_py_from_matrix(mupdf.fz_concat(m1, m2))
def match_string(h0, n0):
h = 0
n = 0
e = h
delta_h, hc = chartocanon(h0[h:])
h += delta_h
delta_n, nc = chartocanon(n0[n:])
n += delta_n
while hc == nc:
e = h
if hc == ord(' '):
while 1:
delta_h, hc = chartocanon(h0[h:])
h += delta_h
if hc != ord(' '):
break
else:
delta_h, hc = chartocanon(h0[h:])
h += delta_h
if nc == ord(' '):
while 1:
delta_n, nc = chartocanon(n0[n:])
n += delta_n
if nc != ord(' '):
break
else:
delta_n, nc = chartocanon(n0[n:])
n += delta_n
return None if nc != 0 else e
def on_highlight_char(hits, line, ch):
assert hits
assert isinstance(line, mupdf.FzStextLine)
assert isinstance(ch, mupdf.FzStextChar)
vfuzz = ch.m_internal.size * hits.vfuzz
hfuzz = ch.m_internal.size * hits.hfuzz
ch_quad = JM_char_quad(line, ch)
if hits.len > 0:
quad = hits.quads[hits.len - 1]
end = JM_quad_from_py(quad)
if ( 1
and hdist(line.m_internal.dir, end.lr, ch_quad.ll) < hfuzz
and vdist(line.m_internal.dir, end.lr, ch_quad.ll) < vfuzz
and hdist(line.m_internal.dir, end.ur, ch_quad.ul) < hfuzz
and vdist(line.m_internal.dir, end.ur, ch_quad.ul) < vfuzz
):
end.ur = ch_quad.ur
end.lr = ch_quad.lr
assert hits.quads[-1] == end
return
hits.quads.append(ch_quad)
hits.len += 1
def page_merge(doc_des, doc_src, page_from, page_to, rotate, links, copy_annots, graft_map):
'''
Deep-copies a source page to the target.
Modified version of function of pdfmerge.c: we also copy annotations, but
we skip some subtypes. In addition we rotate output.
'''
if g_use_extra:
return extra.page_merge( doc_des, doc_src, page_from, page_to, rotate, links, copy_annots, graft_map)
known_page_objs = [
PDF_NAME('Contents'),
PDF_NAME('Resources'),
PDF_NAME('MediaBox'),
PDF_NAME('CropBox'),
PDF_NAME('BleedBox'),
PDF_NAME('TrimBox'),
PDF_NAME('ArtBox'),
PDF_NAME('Rotate'),
PDF_NAME('UserUnit'),
]
page_ref = mupdf.pdf_lookup_page_obj(doc_src, page_from)
page_dict = mupdf.pdf_new_dict(doc_des, 4)
mupdf.pdf_dict_put(page_dict, PDF_NAME('Type'), PDF_NAME('Page'))
for i in range( len(known_page_objs)):
obj = mupdf.pdf_dict_get_inheritable( page_ref, known_page_objs[i])
if obj.m_internal:
mupdf.pdf_dict_put( page_dict, known_page_objs[i], mupdf.pdf_graft_mapped_object(graft_map.this, obj))
if copy_annots:
old_annots = mupdf.pdf_dict_get( page_ref, PDF_NAME('Annots'))
n = mupdf.pdf_array_len( old_annots)
if n > 0:
new_annots = mupdf.pdf_dict_put_array( page_dict, PDF_NAME('Annots'), n)
for i in range(n):
o = mupdf.pdf_array_get( old_annots, i)
if not o.m_internal or mupdf.pdf_is_dict(o):
continue
if mupdf.pdf_dict_gets( o, "IRT").m_internal:
continue
subtype = mupdf.pdf_dict_get( o, PDF_NAME('Subtype'))
if mupdf.pdf_name_eq( subtype, PDF_NAME('Link')):
continue
if mupdf.pdf_name_eq( subtype, PDF_NAME('Popup')):
continue
if mupdf.pdf_name_eq( subtype, PDF_NAME('Widget')):
mupdf.fz_warn( "skipping widget annotation")
continue
if mupdf.pdf_name_eq(subtype, PDF_NAME('Widget')):
continue
mupdf.pdf_dict_del( o, PDF_NAME('Popup'))
mupdf.pdf_dict_del( o, PDF_NAME('P'))
copy_o = mupdf.pdf_graft_mapped_object( graft_map.this, o)
annot = mupdf.pdf_new_indirect( doc_des, mupdf.pdf_to_num( copy_o), 0)
mupdf.pdf_array_push( new_annots, annot)
if rotate != -1:
mupdf.pdf_dict_put_int( page_dict, PDF_NAME('Rotate'), rotate)
ref = mupdf.pdf_add_object( doc_des, page_dict)
mupdf.pdf_insert_page( doc_des, page_to, ref)
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 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_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 pdf_lookup_page_loc_imp(doc, node, skip, parentp, indexp):
assert isinstance(node, mupdf.PdfObj)
assert isinstance(skip, list) and len(skip) == 1
assert isinstance(indexp, list) and len(indexp) == 1
assert isinstance(parentp, list) and len(parentp) == 1 and isinstance(parentp[0], mupdf.PdfObj)
hit = None
stack = []
try:
while 1:
kids = mupdf.pdf_dict_get(node, PDF_NAME('Kids'))
len_ = mupdf.pdf_array_len( kids)
if len_ == 0:
raise Exception("malformed page tree")
stack.append(node)
if mupdf.pdf_mark_obj( node):
raise Exception( "cycle in page tree")
for i in range(len_):
kid = mupdf.pdf_array_get( kids, i)
type_ = mupdf.pdf_dict_get( kid, PDF_NAME('Type'))
if type_.m_internal:
a = mupdf.pdf_name_eq( type_, PDF_NAME('Pages'))
else:
a = (
mupdf.pdf_dict_get( kid, PDF_NAME('Kids')).m_internal
and not mupdf.pdf_dict_get( kid, PDF_NAME('MediaBox')).m_internal
)
if a:
count = mupdf.pdf_dict_get_int( kid, PDF_NAME('Count'))
if (skip[0] < count):
node = kid
break
else:
skip[0] -= count
else:
if type_.m_internal:
a = not mupdf.pdf_name_eq( type_, PDF_NAME('Page'))
else:
a = not mupdf.pdf_dict_get( kid, PDF_NAME('MediaBox')).m_internal
if a:
mupdf.fz_warn( f"non-page object in page tree ({mupdf.pdf_to_name( type_)})")
if skip[0] == 0:
parentp[0] = node
indexp[0] = i
hit = kid
break
else:
skip[0] -= 1
if not ((hit is None or hit.m_internal is None) and i < len_):
break
finally:
for i in range(len(stack), 0, -1):
mupdf.pdf_unmark_obj( stack[i-1])
return hit
def pdf_lookup_page_loc(doc, needle):
'''
Copy of MuPDF's internal pdf_lookup_page_loc().
'''
root = mupdf.pdf_dict_get( mupdf.pdf_trailer( doc), PDF_NAME('Root'))
node = mupdf.pdf_dict_get( root, PDF_NAME('Pages'))
skip = [needle]
if not node.m_internal:
raise Exception("cannot find page tree")
parentp = [mupdf.PdfObj()]
indexp = [0]
hit = pdf_lookup_page_loc_imp(doc, node, skip, parentp, indexp)
skip = skip[0]
parentp = parentp[0]
indexp = indexp[0]
if not hit.m_internal:
raise Exception("cannot find page %d in page tree" % needle+1)
return hit, parentp, indexp
def pdfobj_string(o, prefix=''):
'''
Returns description of mupdf.PdfObj (wrapper for pdf_obj) <o>.
'''
assert 0, 'use mupdf.pdf_debug_obj() ?'
ret = ''
if mupdf.pdf_is_array(o):
l = mupdf.pdf_array_len(o)
ret += f'array {l}\n'
for i in range(l):
oo = mupdf.pdf_array_get(o, i)
ret += pdfobj_string(oo, prefix + ' ')
ret += '\n'
elif mupdf.pdf_is_bool(o):
ret += f'bool: {o.array_get_bool()}\n'
elif mupdf.pdf_is_dict(o):
l = mupdf.pdf_dict_len(o)
ret += f'dict {l}\n'
for i in range(l):
key = mupdf.pdf_dict_get_key(o, i)
value = mupdf.pdf_dict_get( o, key)
ret += f'{prefix} {key}: '
ret += pdfobj_string( value, prefix + ' ')
ret += '\n'
elif mupdf.pdf_is_embedded_file(o):
ret += f'embedded_file: {o.embedded_file_name()}\n'
elif mupdf.pdf_is_indirect(o):
ret += f'indirect: ...\n'
elif mupdf.pdf_is_int(o):
ret += f'int: {mupdf.pdf_to_int(o)}\n'
elif mupdf.pdf_is_jpx_image(o):
ret += f'jpx_image:\n'
elif mupdf.pdf_is_name(o):
ret += f'name: {mupdf.pdf_to_name(o)}\n'
elif o.pdf_is_null:
ret += f'null\n'
elif o.pdf_is_real:
ret += f'real: {o.pdf_to_real()}\n'
elif mupdf.pdf_is_stream(o):
ret += f'stream\n'
elif mupdf.pdf_is_string(o):
ret += f'string: {mupdf.pdf_to_string(o)}\n'
else:
ret += '<>\n'
return ret
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
distance, e.g. "a b c" instead of "abc". This utility adds an entry
"/W[0 65535 w]" to the descendent font(s) of font. The float w is
taken to be the width of 0x20 (space).
This should enforce viewers to use 'w' as the character width.
Args:
page: fitz.Page object.
font: fitz.Font object.
"""
if not font.flags["mono"]:
return None
doc = page.parent
fontlist = page.get_fonts()
xrefs = [
f[0]
for f in fontlist
if (f[3] == font.name and f[4].startswith("F") and f[5].startswith("Identity"))
]
if xrefs == []:
return
xrefs = set(xrefs)
width = int(round((font.glyph_advance(32) * 1000)))
for xref in xrefs:
if not TOOLS.set_font_width(doc, xref, width):
log("Cannot set width for '%s' in xref %i" % (font.name, xref))
def retainpage(doc, parent, kids, page):
'''
Recreate page tree to only retain specified pages.
'''
pageref = mupdf.pdf_lookup_page_obj(doc, page)
mupdf.pdf_flatten_inheritable_page_items(pageref)
mupdf.pdf_dict_put(pageref, PDF_NAME('Parent'), parent)
mupdf.pdf_array_push(kids, pageref)
def retainpages(doc, liste):
'''
This is called by PyMuPDF:
liste = page numbers to retain
'''
argc = len(liste)
pagecount = mupdf.pdf_count_pages(doc)
oldroot = mupdf.pdf_dict_get(mupdf.pdf_trailer(doc), PDF_NAME('Root'))
pages = mupdf.pdf_dict_get(oldroot, PDF_NAME('Pages'))
olddests = mupdf.pdf_load_name_tree(doc, PDF_NAME('Dests'))
outlines = mupdf.pdf_dict_get(oldroot, PDF_NAME('Outlines'))
ocproperties = mupdf.pdf_dict_get(oldroot, PDF_NAME('OCProperties'))
root = mupdf.pdf_new_dict(doc, 3)
mupdf.pdf_dict_put(root, PDF_NAME('Type'), mupdf.pdf_dict_get(oldroot, PDF_NAME('Type')))
mupdf.pdf_dict_put(root, PDF_NAME('Pages'), mupdf.pdf_dict_get(oldroot, PDF_NAME('Pages')))
if outlines.m_internal:
mupdf.pdf_dict_put(root, PDF_NAME('Outlines'), outlines)
if ocproperties.m_internal:
mupdf.pdf_dict_put(root, PDF_NAME('OCProperties'), ocproperties)
mupdf.pdf_update_object(doc, mupdf.pdf_to_num(oldroot), root)
kids = mupdf.pdf_new_array(doc, 1)
for page in range(argc):
i = liste[page]
if i < 0 or i >= pagecount:
RAISEPY(MSG_BAD_PAGENO, PyExc_ValueError)
retainpage(doc, pages, kids, i)
countobj = mupdf.pdf_new_int(mupdf.pdf_array_len(kids))
mupdf.pdf_dict_put(pages, PDF_NAME('Count'), countobj)
mupdf.pdf_dict_put(pages, PDF_NAME('Kids'), kids)
pagecount = mupdf.pdf_count_pages(doc)
page_object_nums = []
for i in range(pagecount):
pageref = mupdf.pdf_lookup_page_obj(doc, i)
page_object_nums.append(mupdf.pdf_to_num(pageref))
if olddests:
names = mupdf.pdf_new_dict(doc, 1)
dests = mupdf.pdf_new_dict(doc, 1)
len_ = mupdf.pdf_dict_len(olddests)
names_list = mupdf.pdf_new_array(doc, 32)
for i in range(len_):
key = mupdf.pdf_dict_get_key(olddests, i)
val = mupdf.pdf_dict_get_val(olddests, i)
dest = mupdf.pdf_dict_get(val, PDF_NAME('D'))
dest = mupdf.pdf_array_get(dest if dest.m_internal else val, 0)
if dest_is_valid_page(dest, page_object_nums, pagecount):
key_str = mupdf.pdf_new_string(mupdf.pdf_to_name(key), len(mupdf.pdf_to_name(key)))
mupdf.pdf_array_push(names_list, key_str)
mupdf.pdf_array_push(names_list, val)
mupdf.pdf_dict_put(dests, PDF_NAME('Names'), names_list)
mupdf.pdf_dict_put(names, PDF_NAME('Dests'), dests)
mupdf.pdf_dict_put(root, PDF_NAME('Names'), names)
for i in range(pagecount):
pageref = mupdf.pdf_lookup_page_obj(doc, i)
annots = mupdf.pdf_dict_get(pageref, PDF_NAME('Annots'))
len_ = mupdf.pdf_array_len(annots)
j = 0
while 1:
if j >= len_:
break
o = mupdf.pdf_array_get(annots, j)
if not mupdf.pdf_name_eq(mupdf.pdf_dict_get(o, PDF_NAME('Subtype')), PDF_NAME('Link')):
continue
if not dest_is_valid(o, pagecount, page_object_nums, names_list):
mupdf.pdf_array_delete(annots, j)
len_ -= 1
j -= 1
j += 1
if strip_outlines( doc, outlines, pagecount, page_object_nums, names_list) == 0:
mupdf.pdf_dict_del(root, PDF_NAME('Outlines'))
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 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 string_in_names_list(p, names_list):
n = mupdf.pdf_array_len( names_list)
str_ = mupdf.pdf_to_text_string( p)
for i in range(0, n, 2):
if mupdf.pdf_to_text_string( mupdf.pdf_array_get( names_list, i)) == str_:
return 1
return 0
def strip_outline(doc, outlines, page_count, page_object_nums, names_list):
'''
Returns (count, first, prev).
'''
first = None
count = 0
current = outlines
prev = None
while current.m_internal:
nc = strip_outlines(doc, current, page_count, page_object_nums, names_list)
if not dest_is_valid(current, page_count, page_object_nums, names_list):
if nc == 0:
next = mupdf.pdf_dict_get(current, PDF_NAME('Next'))
if not next.m_internal:
if prev.m_internal:
mupdf.pdf_dict_del(prev, PDF_NAME('Next'))
elif prev.m_internal:
mupdf.pdf_dict_put(prev, PDF_NAME('Next'), next)
mupdf.pdf_dict_put(next, PDF_NAME('Prev'), prev)
else:
mupdf.pdf_dict_del(next, PDF_NAME('Prev'))
current = next
else:
mupdf.pdf_dict_del(current, PDF_NAME('Dest'))
mupdf.pdf_dict_del(current, PDF_NAME('A'))
current = mupdf.pdf_dict_get(current, PDF_NAME('Next'))
else:
if not first or not first.m_internal:
first = current
prev = current
current = mupdf.pdf_dict_get(current, PDF_NAME('Next'))
count += 1
return count, first, prev
def strip_outlines(doc, outlines, page_count, page_object_nums, names_list):
if not outlines.m_internal:
return 0
first = mupdf.pdf_dict_get(outlines, PDF_NAME('First'))
if not first.m_internal:
nc = 0
else:
nc, first, last = strip_outline(doc, first, page_count, page_object_nums, names_list)
if nc == 0:
mupdf.pdf_dict_del(outlines, PDF_NAME('First'))
mupdf.pdf_dict_del(outlines, PDF_NAME('Last'))
mupdf.pdf_dict_del(outlines, PDF_NAME('Count'))
else:
old_count = mupdf.pdf_to_int(mupdf.pdf_dict_get(outlines, PDF_NAME('Count')))
mupdf.pdf_dict_put(outlines, PDF_NAME('First'), first)
mupdf.pdf_dict_put(outlines, PDF_NAME('Last'), last)
mupdf.pdf_dict_put(outlines, PDF_NAME('Count'), mupdf.pdf_new_int(nc if old_count > 0 else -nc))
return nc
trace_device_FILL_PATH = 1
trace_device_STROKE_PATH = 2
trace_device_CLIP_PATH = 3
trace_device_CLIP_STROKE_PATH = 4
def unicode_to_glyph_name(ch: int) -> str:
'''
Adobe Glyph List function
'''
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 PySys_WriteStdout(text):
sys.stdout.write(text)
def PySys_WriteStderr(text):
sys.stderr.write(text)
def vdist(dir, a, b):
dx = b.x - a.x
dy = b.y - a.y
return mupdf.fz_abs(dx * dir.y + dy * dir.x)
class TOOLS:
'''
We use @staticmethod to avoid the need to create an instance of this class.
'''
def _derotate_matrix(page):
if isinstance(page, mupdf.PdfPage):
return JM_py_from_matrix(JM_derotate_page_matrix(page))
else:
return JM_py_from_matrix(mupdf.FzMatrix())
@staticmethod
def _fill_widget(annot, widget):
val = JM_get_widget_properties(annot, widget)
widget.rect = Rect(annot.rect)
widget.xref = annot.xref
widget.parent = annot.parent
widget._annot = annot
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
@staticmethod
def _get_all_contents(page):
page = mupdf.pdf_page_from_fz_page(page.this)
res = JM_read_contents(page.obj())
result = JM_BinFromBuffer( res)
return result
@staticmethod
def _insert_contents(page, newcont, overlay=1):
"""Add bytes as a new /Contents object for a page, and return its xref."""
pdfpage = page._pdf_page()
ASSERT_PDF(pdfpage)
contbuf = JM_BufferFromBytes(newcont)
xref = JM_insert_contents(pdfpage.doc(), pdfpage.obj(), contbuf, overlay)
return xref
@staticmethod
def _le_annot_parms(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"]
sc = annot.colors["stroke"]
if not sc:
sc = (0,0,0)
scol = " ".join(map(str, sc)) + " RG\n"
if fill_color:
fc = fill_color
else:
fc = annot.colors["fill"]
if not fc:
fc = (1,1,1)
fcol = " ".join(map(str, fc)) + " rg\n"
np1 = p1
np2 = p2
m = Matrix(util_hor_matrix(np1, np2))
im = ~m
L = np1 * m
R = np2 * m
if 0 <= annot.opacity < 1:
opacity = "/H gs\n"
else:
opacity = ""
return m, im, L, R, w, scol, fcol, opacity
@staticmethod
def _le_butt(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 = TOOLS._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
@staticmethod
def _le_circle(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 = TOOLS._le_annot_parms(annot, p1, p2, fill_color)
shift = 2.5
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)
ap = "q\n" + opacity + TOOLS._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
@staticmethod
def _le_closedarrow(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 = TOOLS._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
@staticmethod
def _le_diamond(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 = TOOLS._le_annot_parms(annot, p1, p2, fill_color)
shift = 2.5
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)
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
@staticmethod
def _le_openarrow(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 = TOOLS._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
@staticmethod
def _le_rclosedarrow(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 = TOOLS._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
@staticmethod
def _le_ropenarrow(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 = TOOLS._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
@staticmethod
def _le_slash(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 = TOOLS._le_annot_parms(annot, p1, p2, fill_color)
rw = 1.1547 * max(1, w) * 1.0
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
@staticmethod
def _le_square(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 = TOOLS._le_annot_parms(annot, p1, p2, fill_color)
shift = 2.5
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)
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
@staticmethod
def _oval_string(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
ml = p1 + (p4 - p1) * 0.5
mo = p1 + (p2 - p1) * 0.5
mr = p2 + (p3 - p2) * 0.5
mu = p4 + (p3 - p4) * 0.5
ol1 = ml + (p1 - ml) * kappa
ol2 = mo + (p1 - mo) * kappa
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
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
@staticmethod
def _parse_da(annot):
if g_use_extra:
val = extra.Tools_parse_da( annot.this)
else:
def Tools__parse_da(annot):
this_annot = annot.this
assert isinstance(this_annot, mupdf.PdfAnnot)
this_annot_obj = mupdf.pdf_annot_obj( this_annot)
pdf = mupdf.pdf_get_bound_document( this_annot_obj)
try:
da = mupdf.pdf_dict_get_inheritable( this_annot_obj, PDF_NAME('DA'))
if not da.m_internal:
trailer = mupdf.pdf_trailer(pdf)
da = mupdf.pdf_dict_getl(trailer,
PDF_NAME('Root'),
PDF_NAME('AcroForm'),
PDF_NAME('DA'),
)
da_str = mupdf.pdf_to_text_string(da)
except Exception:
if g_exceptions_verbose: exception_info()
return
return da_str
val = Tools__parse_da(annot)
if not val:
return ((0,), "", 0)
font = "Helv"
fsize = 12
col = (0, 0, 0)
dat = val.split()
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":
col = [(float(dat[i - 1]))]
dat[i] = dat[i-1] = ""
continue
if item == "rg":
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":
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
@staticmethod
def _reset_widget(annot):
this_annot = annot
this_annot_obj = mupdf.pdf_annot_obj(this_annot)
pdf = mupdf.pdf_get_bound_document(this_annot_obj)
mupdf.pdf_field_reset(pdf, this_annot_obj)
@staticmethod
def _rotate_matrix(page):
pdfpage = page._pdf_page()
if not pdfpage.m_internal:
return JM_py_from_matrix(mupdf.FzMatrix())
return JM_py_from_matrix(JM_rotate_page_matrix(pdfpage))
@staticmethod
def _save_widget(annot, widget):
JM_set_widget_properties(annot, widget)
def _update_da(annot, da_str):
if g_use_extra:
extra.Tools_update_da( annot.this, da_str)
else:
try:
this_annot = annot.this
assert isinstance(this_annot, mupdf.PdfAnnot)
mupdf.pdf_dict_put_text_string(mupdf.pdf_annot_obj(this_annot), PDF_NAME('DA'), da_str)
mupdf.pdf_dict_del(mupdf.pdf_annot_obj(this_annot), PDF_NAME('DS'))
mupdf.pdf_dict_del(mupdf.pdf_annot_obj(this_annot), PDF_NAME('RC'))
except Exception:
if g_exceptions_verbose: exception_info()
return
return
@staticmethod
def gen_id():
global TOOLS_JM_UNIQUE_ID
TOOLS_JM_UNIQUE_ID += 1
return TOOLS_JM_UNIQUE_ID
@staticmethod
def glyph_cache_empty():
'''
Empty the glyph cache.
'''
mupdf.fz_purge_glyph_cache()
@staticmethod
def image_profile(stream, keep_image=0):
'''
Metadata of an image binary stream.
'''
return JM_image_profile(stream, keep_image)
@staticmethod
def mupdf_display_errors(on=None):
'''
Set MuPDF error display to True or False.
'''
global JM_mupdf_show_errors
if on is not None:
JM_mupdf_show_errors = bool(on)
return JM_mupdf_show_errors
@staticmethod
def mupdf_display_warnings(on=None):
'''
Set MuPDF warnings display to True or False.
'''
global JM_mupdf_show_warnings
if on is not None:
JM_mupdf_show_warnings = bool(on)
return JM_mupdf_show_warnings
@staticmethod
def mupdf_version():
'''Get version of MuPDF binary build.'''
return mupdf.FZ_VERSION
@staticmethod
def mupdf_warnings(reset=1):
'''
Get the MuPDF warnings/errors with optional reset (default).
'''
mupdf.fz_flush_warnings()
ret = '\n'.join( JM_mupdf_warnings_store)
if reset:
TOOLS.reset_mupdf_warnings()
return ret
@staticmethod
def reset_mupdf_warnings():
global JM_mupdf_warnings_store
JM_mupdf_warnings_store = list()
@staticmethod
def set_aa_level(level):
'''
Set anti-aliasing level.
'''
mupdf.fz_set_aa_level(level)
@staticmethod
def set_annot_stem( stem=None):
global JM_annot_id_stem
if stem is None:
return JM_annot_id_stem
len_ = len(stem) + 1
if len_ > 50:
len_ = 50
JM_annot_id_stem = stem[:50]
return JM_annot_id_stem
@staticmethod
def set_font_width(doc, xref, width):
pdf = _as_pdf_document(doc)
if not pdf:
return False
font = mupdf.pdf_load_object(pdf, xref)
dfonts = mupdf.pdf_dict_get(font, PDF_NAME('DescendantFonts'))
if mupdf.pdf_is_array(dfonts):
n = mupdf.pdf_array_len(dfonts)
for i in range(n):
dfont = mupdf.pdf_array_get(dfonts, i)
warray = mupdf.pdf_new_array(pdf, 3)
mupdf.pdf_array_push(warray, mupdf.pdf_new_int(0))
mupdf.pdf_array_push(warray, mupdf.pdf_new_int(65535))
mupdf.pdf_array_push(warray, mupdf.pdf_new_int(width))
mupdf.pdf_dict_put(dfont, PDF_NAME('W'), warray)
return True
@staticmethod
def set_graphics_min_line_width(min_line_width):
'''
Set the graphics minimum line width.
'''
mupdf.fz_set_graphics_min_line_width(min_line_width)
@staticmethod
def set_icc( on=0):
"""Set ICC color handling on or off."""
if on:
if mupdf.FZ_ENABLE_ICC:
mupdf.fz_enable_icc()
else:
RAISEPY( "MuPDF built w/o ICC support",PyExc_ValueError)
elif mupdf.FZ_ENABLE_ICC:
mupdf.fz_disable_icc()
@staticmethod
def set_low_memory( on=None):
"""Set / unset MuPDF device caching."""
global g_no_device_caching
if on is not None:
g_no_device_caching = bool(on)
return g_no_device_caching
@staticmethod
def set_small_glyph_heights(on=None):
"""Set / unset small glyph heights."""
global g_small_glyph_heights
if on is not None:
g_small_glyph_heights = bool(on)
extra.set_small_glyph_heights( g_small_glyph_heights)
return g_small_glyph_heights
@staticmethod
def set_subset_fontnames(on=None):
'''
Set / unset returning fontnames with their subset prefix.
'''
global g_subset_fontnames
if on is not None:
g_subset_fontnames = bool(on)
return g_subset_fontnames
@staticmethod
def show_aa_level():
'''
Show anti-aliasing values.
'''
return dict(
graphics = mupdf.fz_graphics_aa_level(),
text = mupdf.fz_text_aa_level(),
graphics_min_line_width = mupdf.fz_graphics_min_line_width(),
)
@staticmethod
def store_maxsize():
'''
MuPDF store size limit.
'''
return None
@staticmethod
def store_shrink(percent):
'''
Free 'percent' of current store size.
'''
if percent >= 100:
mupdf.fz_empty_store()
return 0
if percent > 0:
mupdf.fz_shrink_store( 100 - percent)
@staticmethod
def store_size():
'''
MuPDF current store size.
'''
return None
@staticmethod
def unset_quad_corrections(on=None):
'''
Set ascender / descender corrections on or off.
'''
global g_skip_quad_corrections
if on is not None:
g_skip_quad_corrections = bool(on)
return g_skip_quad_corrections
JM_annot_id_stem = 'fitz'
fitz_config = JM_fitz_config()
from . import utils
pdfcolor = dict(
[
(k, (r / 255, g / 255, b / 255))
for k, (r, g, b) in utils.getColorInfoDict().items()
]
)
if not mupdf_cppyy:
mupdf.fz_set_warning_callback(JM_mupdf_warning)
mupdf.fz_set_error_callback(JM_mupdf_error)
def _atexit():
mupdf.fz_flush_warnings()
mupdf.fz_set_warning_callback(None)
mupdf.fz_set_error_callback(None)
atexit.register( _atexit)
recover_bbox_quad = utils.recover_bbox_quad
recover_char_quad = utils.recover_char_quad
recover_line_quad = utils.recover_line_quad
recover_quad = utils.recover_quad
recover_span_quad = utils.recover_span_quad
Annot.get_text = utils.get_text
Annot.get_textbox = utils.get_textbox
Document._do_links = utils.do_links
Document.del_toc_item = utils.del_toc_item
Document.get_char_widths = utils.get_char_widths
Document.get_oc = utils.get_oc
Document.get_ocmd = utils.get_ocmd
Document.get_page_labels = utils.get_page_labels
Document.get_page_numbers = utils.get_page_numbers
Document.get_page_pixmap = utils.get_page_pixmap
Document.get_page_text = utils.get_page_text
Document.get_toc = utils.get_toc
Document.has_annots = utils.has_annots
Document.has_links = utils.has_links
Document.insert_page = utils.insert_page
Document.new_page = utils.new_page
Document.scrub = utils.scrub
Document.search_page_for = utils.search_page_for
Document.set_metadata = utils.set_metadata
Document.set_oc = utils.set_oc
Document.set_ocmd = utils.set_ocmd
Document.set_page_labels = utils.set_page_labels
Document.set_toc = utils.set_toc
Document.set_toc_item = utils.set_toc_item
Document.subset_fonts = utils.subset_fonts
Document.tobytes = Document.write
IRect.get_area = utils.get_area
Page.apply_redactions = utils.apply_redactions
Page.delete_image = utils.delete_image
Page.delete_widget = utils.delete_widget
Page.draw_bezier = utils.draw_bezier
Page.draw_circle = utils.draw_circle
Page.draw_curve = utils.draw_curve
Page.draw_line = utils.draw_line
Page.draw_oval = utils.draw_oval
Page.draw_polyline = utils.draw_polyline
Page.draw_quad = utils.draw_quad
Page.draw_rect = utils.draw_rect
Page.draw_sector = utils.draw_sector
Page.draw_squiggle = utils.draw_squiggle
Page.draw_zigzag = utils.draw_zigzag
Page.get_image_info = utils.get_image_info
Page.get_image_rects = utils.get_image_rects
Page.get_label = utils.get_label
Page.get_links = utils.get_links
Page.get_pixmap = utils.get_pixmap
Page.get_text = utils.get_text
Page.get_text_blocks = utils.get_text_blocks
Page.get_text_selection = utils.get_text_selection
Page.get_text_words = utils.get_text_words
Page.get_textbox = utils.get_textbox
Page.get_textpage_ocr = utils.get_textpage_ocr
Page.insert_image = utils.insert_image
Page.insert_link = utils.insert_link
Page.insert_text = utils.insert_text
Page.insert_textbox = utils.insert_textbox
Page.new_shape = lambda x: utils.Shape(x)
Page.replace_image = utils.replace_image
Page.search_for = utils.search_for
Page.show_pdf_page = utils.show_pdf_page
Page.update_link = utils.update_link
Page.write_text = utils.write_text
from .table import find_tables
Page.find_tables = find_tables
Rect.get_area = utils.get_area
TextWriter.fill_textbox = utils.fill_textbox
class FitzDeprecation(DeprecationWarning):
pass
VersionFitz = "1.23.7"
VersionBind = "1.23.7"
VersionDate = "2023-11-30 00:00:01"
VersionDate2 = VersionDate.replace('-', '').replace(' ', '').replace(':', '')
version = (VersionBind, VersionFitz, VersionDate2)
pymupdf_version_tuple = tuple( [int(i) for i in VersionFitz.split('.')])
def restore_aliases():
warnings.filterwarnings( "once", category=FitzDeprecation)
def showthis(msg, cat, filename, lineno, file=None, line=None):
text = warnings.formatwarning(msg, cat, filename, lineno, line=line)
s = text.find("FitzDeprecation")
if s < 0:
log(text, file=sys.stderr)
return
text = text[s:].splitlines()[0][4:]
log(text, file=sys.stderr)
warnings.showwarning = showthis
def _alias(class_, new_name, legacy_name=None):
'''
Adds an alias for a class_ or module item clled <class_>.<new>.
class_:
Class/module to modify; use None for the current module.
new_name:
String name of existing item, e.g. name of method.
legacy_name:
Name of legacy object to create in <class_>. If None, we generate
from <item> by removing underscores and capitalising the next
letter.
'''
if class_ is None:
class_ = sys.modules[__name__]
if not legacy_name:
legacy_name = ''
capitalise_next = False
for c in new_name:
if c == '_':
capitalise_next = True
elif capitalise_next:
legacy_name += c.upper()
capitalise_next = False
else:
legacy_name += c
new_object = getattr( class_, new_name)
assert not getattr( class_, legacy_name, None), f'class {class_} already has {legacy_name}'
if callable( new_object):
def deprecated_function( *args, **kwargs):
if not VersionBind.startswith('1.18'):
warnings.warn(
f'"{legacy_name}" removed from {class_} after v1.19.0 - use "{new_name}".',
category=FitzDeprecation,
)
return new_object( *args, **kwargs)
setattr( class_, legacy_name, deprecated_function)
deprecated_function.__doc__ = (
f'*** Deprecated and removed in version following 1.19.0 - use "{new_name}". ***\n'
f'{new_object.__doc__}'
)
else:
setattr( class_, legacy_name, new_object)
_alias( Annot, 'get_file', 'fileGet')
_alias( Annot, 'get_pixmap')
_alias( Annot, 'get_sound', 'soundGet')
_alias( Annot, 'get_text')
_alias( Annot, 'get_textbox')
_alias( Annot, 'get_textpage', 'getTextPage')
_alias( Annot, 'line_ends')
_alias( Annot, 'set_blendmode', 'setBlendMode')
_alias( Annot, 'set_border')
_alias( Annot, 'set_colors')
_alias( Annot, 'set_flags')
_alias( Annot, 'set_info')
_alias( Annot, 'set_line_ends')
_alias( Annot, 'set_name')
_alias( Annot, 'set_oc', 'setOC')
_alias( Annot, 'set_opacity')
_alias( Annot, 'set_rect')
_alias( Annot, 'update_file', 'fileUpd')
_alias( DisplayList, 'get_pixmap')
_alias( DisplayList, 'get_textpage', 'getTextPage')
_alias( Document, 'chapter_count')
_alias( Document, 'chapter_page_count')
_alias( Document, 'convert_to_pdf', 'convertToPDF')
_alias( Document, 'copy_page')
_alias( Document, 'delete_page')
_alias( Document, 'delete_pages', 'deletePageRange')
_alias( Document, 'embfile_add', 'embeddedFileAdd')
_alias( Document, 'embfile_count', 'embeddedFileCount')
_alias( Document, 'embfile_del', 'embeddedFileDel')
_alias( Document, 'embfile_get', 'embeddedFileGet')
_alias( Document, 'embfile_info', 'embeddedFileInfo')
_alias( Document, 'embfile_names', 'embeddedFileNames')
_alias( Document, 'embfile_upd', 'embeddedFileUpd')
_alias( Document, 'extract_font')
_alias( Document, 'extract_image')
_alias( Document, 'find_bookmark')
_alias( Document, 'fullcopy_page')
_alias( Document, 'get_char_widths')
_alias( Document, 'get_ocgs', 'getOCGs')
_alias( Document, 'get_page_fonts', 'getPageFontList')
_alias( Document, 'get_page_images', 'getPageImageList')
_alias( Document, 'get_page_pixmap')
_alias( Document, 'get_page_text')
_alias( Document, 'get_page_xobjects', 'getPageXObjectList')
_alias( Document, 'get_sigflags', 'getSigFlags')
_alias( Document, 'get_toc', 'getToC')
_alias( Document, 'get_xml_metadata')
_alias( Document, 'insert_page')
_alias( Document, 'insert_pdf', 'insertPDF')
_alias( Document, 'is_dirty')
_alias( Document, 'is_form_pdf', 'isFormPDF')
_alias( Document, 'is_pdf', 'isPDF')
_alias( Document, 'is_reflowable')
_alias( Document, 'is_repaired')
_alias( Document, 'last_location')
_alias( Document, 'load_page')
_alias( Document, 'make_bookmark')
_alias( Document, 'move_page')
_alias( Document, 'needs_pass')
_alias( Document, 'new_page')
_alias( Document, 'next_location')
_alias( Document, 'page_count')
_alias( Document, 'page_cropbox', 'pageCropBox')
_alias( Document, 'page_xref')
_alias( Document, 'pdf_catalog', 'PDFCatalog')
_alias( Document, 'pdf_trailer', 'PDFTrailer')
_alias( Document, 'prev_location', 'previousLocation')
_alias( Document, 'resolve_link')
_alias( Document, 'search_page_for')
_alias( Document, 'set_language')
_alias( Document, 'set_metadata')
_alias( Document, 'set_toc', 'setToC')
_alias( Document, 'set_xml_metadata')
_alias( Document, 'update_object')
_alias( Document, 'update_stream')
_alias( Document, 'xref_is_stream', 'isStream')
_alias( Document, 'xref_length')
_alias( Document, 'xref_object')
_alias( Document, 'xref_stream')
_alias( Document, 'xref_stream_raw')
_alias( Document, 'xref_xml_metadata', 'metadataXML')
_alias( IRect, 'get_area')
_alias( IRect, 'get_area', 'getRectArea')
_alias( IRect, 'include_point')
_alias( IRect, 'include_rect')
_alias( IRect, 'is_empty')
_alias( IRect, 'is_infinite')
_alias( Link, 'is_external')
_alias( Link, 'set_border')
_alias( Link, 'set_colors')
_alias( Matrix, 'is_rectilinear')
_alias( Matrix, 'prerotate', 'preRotate')
_alias( Matrix, 'prescale', 'preScale')
_alias( Matrix, 'preshear', 'preShear')
_alias( Matrix, 'pretranslate', 'preTranslate')
_alias( None, 'get_pdf_now', 'getPDFnow')
_alias( None, 'get_pdf_str', 'getPDFstr')
_alias( None, 'get_text_length')
_alias( None, 'get_text_length', 'getTextlength')
_alias( None, 'image_profile', 'ImageProperties')
_alias( None, 'paper_rect', 'PaperRect')
_alias( None, 'paper_size', 'PaperSize')
_alias( None, 'paper_sizes')
_alias( None, 'planish_line')
_alias( Outline, 'is_external')
_alias( Outline, 'is_open')
_alias( Page, 'add_caret_annot')
_alias( Page, 'add_circle_annot')
_alias( Page, 'add_file_annot')
_alias( Page, 'add_freetext_annot')
_alias( Page, 'add_highlight_annot')
_alias( Page, 'add_ink_annot')
_alias( Page, 'add_line_annot')
_alias( Page, 'add_polygon_annot')
_alias( Page, 'add_polyline_annot')
_alias( Page, 'add_rect_annot')
_alias( Page, 'add_redact_annot')
_alias( Page, 'add_squiggly_annot')
_alias( Page, 'add_stamp_annot')
_alias( Page, 'add_strikeout_annot')
_alias( Page, 'add_text_annot')
_alias( Page, 'add_underline_annot')
_alias( Page, 'add_widget')
_alias( Page, 'clean_contents')
_alias( Page, 'cropbox', 'CropBox')
_alias( Page, 'cropbox_position', 'CropBoxPosition')
_alias( Page, 'delete_annot')
_alias( Page, 'delete_link')
_alias( Page, 'delete_widget')
_alias( Page, 'derotation_matrix')
_alias( Page, 'draw_bezier')
_alias( Page, 'draw_circle')
_alias( Page, 'draw_curve')
_alias( Page, 'draw_line')
_alias( Page, 'draw_oval')
_alias( Page, 'draw_polyline')
_alias( Page, 'draw_quad')
_alias( Page, 'draw_rect')
_alias( Page, 'draw_sector')
_alias( Page, 'draw_squiggle')
_alias( Page, 'draw_zigzag')
_alias( Page, 'first_annot')
_alias( Page, 'first_link')
_alias( Page, 'first_widget')
_alias( Page, 'get_contents')
_alias( Page, 'get_displaylist', 'getDisplayList')
_alias( Page, 'get_drawings')
_alias( Page, 'get_fonts', 'getFontList')
_alias( Page, 'get_image_bbox')
_alias( Page, 'get_images', 'getImageList')
_alias( Page, 'get_links')
_alias( Page, 'get_pixmap')
_alias( Page, 'get_svg_image', 'getSVGimage')
_alias( Page, 'get_text')
_alias( Page, 'get_text_blocks')
_alias( Page, 'get_text_words')
_alias( Page, 'get_textbox')
_alias( Page, 'get_textpage', 'getTextPage')
_alias( Page, 'insert_font')
_alias( Page, 'insert_image')
_alias( Page, 'insert_link')
_alias( Page, 'insert_text')
_alias( Page, 'insert_textbox')
_alias( Page, 'is_wrapped', '_isWrapped')
_alias( Page, 'load_annot')
_alias( Page, 'load_links')
_alias( Page, 'mediabox', 'MediaBox')
_alias( Page, 'mediabox_size', 'MediaBoxSize')
_alias( Page, 'new_shape')
_alias( Page, 'read_contents')
_alias( Page, 'rotation_matrix')
_alias( Page, 'search_for')
_alias( Page, 'set_cropbox', 'setCropBox')
_alias( Page, 'set_mediabox', 'setMediaBox')
_alias( Page, 'set_rotation')
_alias( Page, 'show_pdf_page', 'showPDFpage')
_alias( Page, 'transformation_matrix')
_alias( Page, 'update_link')
_alias( Page, 'wrap_contents')
_alias( Page, 'write_text')
_alias( Pixmap, 'clear_with')
_alias( Pixmap, 'copy', 'copyPixmap')
_alias( Pixmap, 'gamma_with')
_alias( Pixmap, 'invert_irect', 'invertIRect')
_alias( Pixmap, 'pil_save', 'pillowWrite')
_alias( Pixmap, 'pil_tobytes', 'pillowData')
_alias( Pixmap, 'save', 'writeImage')
_alias( Pixmap, 'save', 'writePNG')
_alias( Pixmap, 'set_alpha')
_alias( Pixmap, 'set_dpi', 'setResolution')
_alias( Pixmap, 'set_origin')
_alias( Pixmap, 'set_pixel')
_alias( Pixmap, 'set_rect')
_alias( Pixmap, 'tint_with')
_alias( Pixmap, 'tobytes', 'getImageData')
_alias( Pixmap, 'tobytes', 'getPNGData')
_alias( Pixmap, 'tobytes', 'getPNGdata')
_alias( Quad, 'is_convex')
_alias( Quad, 'is_empty')
_alias( Quad, 'is_rectangular')
_alias( Rect, 'get_area')
_alias( Rect, 'get_area', 'getRectArea')
_alias( Rect, 'include_point')
_alias( Rect, 'include_rect')
_alias( Rect, 'is_empty')
_alias( Rect, 'is_infinite')
_alias( TextWriter, 'fill_textbox')
_alias( TextWriter, 'write_text')
_alias( utils.Shape, 'draw_bezier')
_alias( utils.Shape, 'draw_circle')
_alias( utils.Shape, 'draw_curve')
_alias( utils.Shape, 'draw_line')
_alias( utils.Shape, 'draw_oval')
_alias( utils.Shape, 'draw_polyline')
_alias( utils.Shape, 'draw_quad')
_alias( utils.Shape, 'draw_rect')
_alias( utils.Shape, 'draw_sector')
_alias( utils.Shape, 'draw_squiggle')
_alias( utils.Shape, 'draw_zigzag')
_alias( utils.Shape, 'insert_text')
_alias( utils.Shape, 'insert_textbox')
if 0:
restore_aliases()
__version__ = VersionBind