modm_data.pdf
PDF Content Accessors
This module extends the pypdfium2 Python API with low-level accessors for characters and graphics. Note that these modules support read-only access to PDFs, since a lot of caching is used to speed up commonly accessed properties.
This module only contains formatting independent PDF access which is then
specialized in the vendor-specific modm_data.pdf2html modules.
1# Copyright 2022, Niklas Hauser 2# SPDX-License-Identifier: MPL-2.0 3 4""" 5# PDF Content Accessors 6 7This module extends the pypdfium2 Python API with low-level accessors for 8characters and graphics. Note that these modules support read-only access to 9PDFs, since a lot of caching is used to speed up commonly accessed properties. 10 11This module only contains formatting independent PDF access which is then 12specialized in the vendor-specific `modm_data.pdf2html` modules. 13""" 14 15from .document import Document 16from .page import Page 17from .character import Character 18from .link import ObjLink, WebLink 19from .path import Path 20from .image import Image 21from .render import annotate_debug_info 22from .structure import Structure 23 24__all__ = [ 25 "annotate_debug_info", 26 "Document", 27 "Page", 28 "Character", 29 "Path", 30 "Image", 31 "ObjLink", 32 "WebLink", 33 "Structure", 34]
52def annotate_debug_info(page: Page, new_doc: pp.PdfDocument = None, index: int = 0) -> pp.PdfDocument: 53 """ 54 Copies each page into a new or existing PDF document and overlays the internal information on top of the content. 55 - Renders the bounding boxes in RED and origins in BLACK of all characters. 56 - Renders the bounding boxes of web links in BLUE GREEN. 57 - Renders the bounding boxes of object links in YELLOW GREEN. 58 - Renders all graphics paths in BLUE. 59 - Renders the bounding boxes of computed graphics clusters in CYAN. 60 61 :param page: The page to be annotated. 62 :param new_doc: The PDF document to copy the page to. If not provided, a new document is created. 63 :param index: The index of the page in the new document. 64 :return: The new document with the annotated page added. 65 """ 66 _, height = page.width, page.height 67 68 if new_doc is None: 69 new_doc = pp.raw.FPDF_CreateNewDocument() 70 # copy page over to new doc 71 assert pp.raw.FPDF_ImportPages(new_doc, page.pdf, str(page.number).encode("ascii"), index) 72 new_page = pp.raw.FPDF_LoadPage(new_doc, index) 73 rotation = page.rotation 74 75 for path in page.paths: 76 p0 = path.points[0] 77 if rotation: 78 obj = pp.raw.FPDFPageObj_CreateNewPath(height - p0.y, p0.x) 79 else: 80 obj = pp.raw.FPDFPageObj_CreateNewPath(p0.x, p0.y) 81 assert pp.raw.FPDFPageObj_SetStrokeColor(obj, 0, 0, 0xFF, 0xC0) 82 assert pp.raw.FPDFPageObj_SetStrokeWidth(obj, 0.25) 83 assert pp.raw.FPDFPageObj_SetLineJoin(obj, pp.raw.FPDF_LINEJOIN_ROUND) 84 assert pp.raw.FPDFPageObj_SetLineCap(obj, pp.raw.FPDF_LINECAP_ROUND) 85 assert pp.raw.FPDFPath_SetDrawMode(obj, 0, True) 86 for point in path.points[1:]: 87 if point.type == path.Type.MOVE: 88 if rotation: 89 assert pp.raw.FPDFPath_MoveTo(obj, height - point.y, point.x) 90 else: 91 assert pp.raw.FPDFPath_MoveTo(obj, point.x, point.y) 92 else: 93 if rotation: 94 assert pp.raw.FPDFPath_LineTo(obj, height - point.y, point.x) 95 else: 96 assert pp.raw.FPDFPath_LineTo(obj, point.x, point.y) 97 pp.raw.FPDFPage_InsertObject(new_page, obj) 98 99 for bbox, _ in page.graphic_clusters(): 100 _rect(new_page, rotation, bbox, width=2, stroke=0x00FFFF) 101 102 for link in page.objlinks: 103 _rect(new_page, rotation, link.bbox, width=0.75, stroke=0x9ACD32) 104 105 for link in page.weblinks: 106 for bbox in link.bboxes: 107 _rect(new_page, rotation, bbox, width=0.75, stroke=0x00FF00) 108 109 for char in page.chars: 110 color = 0x0000FF 111 if char.bbox.width: 112 _rect(new_page, rotation, char.bbox, width=0.5, stroke=0xFF0000) 113 _vline( 114 new_page, 115 rotation, 116 char.bbox.midpoint.x, 117 char.bbox.midpoint.y - 1, 118 char.bbox.midpoint.y + 1, 119 width=0.25, 120 stroke=0xFF0000, 121 ) 122 _hline( 123 new_page, 124 rotation, 125 char.bbox.midpoint.y, 126 char.bbox.midpoint.x - 1, 127 char.bbox.midpoint.x + 1, 128 width=0.25, 129 stroke=0xFF0000, 130 ) 131 color = 0x000000 132 _vline(new_page, rotation, char.origin.x, char.origin.y - 1, char.origin.y + 1, width=0.25, stroke=color) 133 _hline(new_page, rotation, char.origin.y, char.origin.x - 1, char.origin.x + 1, width=0.25, stroke=color) 134 135 assert pp.raw.FPDFPage_GenerateContent(new_page) 136 pp.raw.FPDF_ClosePage(new_page) 137 return new_doc
Copies each page into a new or existing PDF document and overlays the internal information on top of the content.
- Renders the bounding boxes in RED and origins in BLACK of all characters.
- Renders the bounding boxes of web links in BLUE GREEN.
- Renders the bounding boxes of object links in YELLOW GREEN.
- Renders all graphics paths in BLUE.
- Renders the bounding boxes of computed graphics clusters in CYAN.
Parameters
- page: The page to be annotated.
- new_doc: The PDF document to copy the page to. If not provided, a new document is created.
- index: The index of the page in the new document.
Returns
The new document with the annotated page added.
33class Document(pp.PdfDocument): 34 """ 35 The PDF document is the root of the entire data structure and provides 36 access to PDF metadata, the table of contents, as well as individual 37 pages. 38 39 You should extend from this class for a specific vendor to provide the 40 correct page class from `page()` function. 41 42 This class is a convenience wrapper with caching around the high-level APIs 43 of pypdfium. 44 """ 45 46 def __init__(self, path: Path, autoclose: bool = False): 47 """ 48 :param path: Path to the PDF to open. 49 """ 50 path = Path(path) 51 self.name: str = path.stem 52 """Stem of the document file name""" 53 super().__init__(path, autoclose=autoclose) 54 self._path = path 55 self._glyph_offsets: dict[str, int] = {} 56 _LOGGER.debug(f"Loading: {path}") 57 58 @cached_property 59 def metadata(self) -> dict[str, str]: 60 """The PDF metadata dictionary.""" 61 return self.get_metadata_dict() 62 63 @property 64 def destinations(self) -> Iterator[tuple[int, str]]: 65 """Yields (page 0-index, named destination) of the whole document.""" 66 for ii in range(pp.raw.FPDF_CountNamedDests(self)): 67 clength = ctypes.c_long() 68 pp.raw.FPDF_GetNamedDest(self, ii, None, clength) 69 cbuffer = ctypes.create_string_buffer(clength.value) 70 dest = pp.raw.FPDF_GetNamedDest(self, ii, cbuffer, clength) 71 name = cbuffer.raw[: clength.value].decode("utf-16-le").rstrip("\x00") 72 page = pp.raw.FPDFDest_GetDestPageIndex(self, dest) 73 yield (page, name) 74 75 @cached_property 76 def toc(self) -> list[_OutlineItem]: 77 """ 78 The table of content as a sorted list of outline items ensuring item has 79 a page index by reusing the last one. 80 """ 81 tocs = set() 82 # Sometimes the TOC contains duplicates so we must use a set 83 last_page_index = 0 84 for toc in self.get_toc(): 85 dest = toc.get_dest() 86 last_page_index = (dest and dest.get_index()) or last_page_index 87 tocs.add(_OutlineItem(toc.level, toc.get_title(), last_page_index)) 88 return list(sorted(list(tocs), key=lambda o: (o.page_index, o.level, o.title))) 89 90 @cached_property 91 def identifier_permanent(self) -> str: 92 """The permanent file identifier.""" 93 return self.get_identifier(pp.raw.FILEIDTYPE_PERMANENT) 94 95 @cached_property 96 def identifier_changing(self) -> str: 97 """The changing file identifier.""" 98 return self.get_identifier(pp.raw.FILEIDTYPE_CHANGING) 99 100 @cached_property 101 def page_count(self) -> int: 102 """The number of pages in the document.""" 103 return pp.raw.FPDF_GetPageCount(self) 104 105 @cache 106 def page(self, index: int) -> Page: 107 """ 108 :param index: 0-indexed page number. 109 :return: the page object for the index. 110 """ 111 assert index < self.page_count 112 return Page(self, index) 113 114 def pages(self, numbers: Iterable[int] = None) -> Iterator[Page]: 115 """ 116 :param numbers: an iterable range of page numbers (0-indexed!). 117 If `None`, then the whole page range is used. 118 :return: yields each page in the range. 119 """ 120 if numbers is None: 121 numbers = range(self.page_count) 122 for ii in numbers: 123 if 0 <= ii < self.page_count: 124 yield self.page(ii) 125 126 def __repr__(self) -> str: 127 return f"Doc({self.name})"
The PDF document is the root of the entire data structure and provides access to PDF metadata, the table of contents, as well as individual pages.
You should extend from this class for a specific vendor to provide the
correct page class from page() function.
This class is a convenience wrapper with caching around the high-level APIs of pypdfium.
46 def __init__(self, path: Path, autoclose: bool = False): 47 """ 48 :param path: Path to the PDF to open. 49 """ 50 path = Path(path) 51 self.name: str = path.stem 52 """Stem of the document file name""" 53 super().__init__(path, autoclose=autoclose) 54 self._path = path 55 self._glyph_offsets: dict[str, int] = {} 56 _LOGGER.debug(f"Loading: {path}")
Parameters
- path: Path to the PDF to open.
58 @cached_property 59 def metadata(self) -> dict[str, str]: 60 """The PDF metadata dictionary.""" 61 return self.get_metadata_dict()
The PDF metadata dictionary.
63 @property 64 def destinations(self) -> Iterator[tuple[int, str]]: 65 """Yields (page 0-index, named destination) of the whole document.""" 66 for ii in range(pp.raw.FPDF_CountNamedDests(self)): 67 clength = ctypes.c_long() 68 pp.raw.FPDF_GetNamedDest(self, ii, None, clength) 69 cbuffer = ctypes.create_string_buffer(clength.value) 70 dest = pp.raw.FPDF_GetNamedDest(self, ii, cbuffer, clength) 71 name = cbuffer.raw[: clength.value].decode("utf-16-le").rstrip("\x00") 72 page = pp.raw.FPDFDest_GetDestPageIndex(self, dest) 73 yield (page, name)
Yields (page 0-index, named destination) of the whole document.
75 @cached_property 76 def toc(self) -> list[_OutlineItem]: 77 """ 78 The table of content as a sorted list of outline items ensuring item has 79 a page index by reusing the last one. 80 """ 81 tocs = set() 82 # Sometimes the TOC contains duplicates so we must use a set 83 last_page_index = 0 84 for toc in self.get_toc(): 85 dest = toc.get_dest() 86 last_page_index = (dest and dest.get_index()) or last_page_index 87 tocs.add(_OutlineItem(toc.level, toc.get_title(), last_page_index)) 88 return list(sorted(list(tocs), key=lambda o: (o.page_index, o.level, o.title)))
The table of content as a sorted list of outline items ensuring item has a page index by reusing the last one.
90 @cached_property 91 def identifier_permanent(self) -> str: 92 """The permanent file identifier.""" 93 return self.get_identifier(pp.raw.FILEIDTYPE_PERMANENT)
The permanent file identifier.
95 @cached_property 96 def identifier_changing(self) -> str: 97 """The changing file identifier.""" 98 return self.get_identifier(pp.raw.FILEIDTYPE_CHANGING)
The changing file identifier.
100 @cached_property 101 def page_count(self) -> int: 102 """The number of pages in the document.""" 103 return pp.raw.FPDF_GetPageCount(self)
The number of pages in the document.
105 @cache 106 def page(self, index: int) -> Page: 107 """ 108 :param index: 0-indexed page number. 109 :return: the page object for the index. 110 """ 111 assert index < self.page_count 112 return Page(self, index)
Parameters
- index: 0-indexed page number.
Returns
the page object for the index.
114 def pages(self, numbers: Iterable[int] = None) -> Iterator[Page]: 115 """ 116 :param numbers: an iterable range of page numbers (0-indexed!). 117 If `None`, then the whole page range is used. 118 :return: yields each page in the range. 119 """ 120 if numbers is None: 121 numbers = range(self.page_count) 122 for ii in numbers: 123 if 0 <= ii < self.page_count: 124 yield self.page(ii)
Parameters
- numbers: an iterable range of page numbers (0-indexed!).
If
None, then the whole page range is used.
Returns
yields each page in the range.
Inherited Members
- pypdfium2._helpers.document.PdfDocument
- parent
- new
- formenv
- init_forms
- close_forms
- get_formtype
- get_pagemode
- is_tagged
- save
- get_identifier
- get_version
- get_metadata_value
- METADATA_KEYS
- get_metadata_dict
- count_attachments
- get_attachment
- new_attachment
- del_attachment
- get_page
- new_page
- del_page
- import_pages
- get_page_size
- get_page_label
- page_as_xobject
- get_toc
- pypdfium2.internal.bases.AutoCloseable
- close
25class Page(pp.PdfPage): 26 """ 27 This class provides low-level access to graphics and characters of the page 28 and allows searching for characters in an area instead of just text. 29 """ 30 31 def __init__(self, document: "modm_data.pdf.Document", index: int): # noqa: F821 32 """ 33 :param document: a PDF document. 34 :param index: 0-index page number. 35 """ 36 self.index = index 37 """0-index page number.""" 38 self.number = index + 1 39 """1-index page number.""" 40 41 super().__init__(pp.raw.FPDF_LoadPage(document, index), document, document.formenv) 42 document._add_kid(self) 43 self._links = None 44 self._weblinks = None 45 self._linked = False 46 47 _LOGGER.debug(f"Loading: {index}") 48 49 self._text = self.get_textpage() 50 self._linkpage = pp.raw.FPDFLink_LoadWebLinks(self._text) 51 self._structtree = pp.raw.FPDF_StructTree_GetForPage(self) 52 # close them in reverse order 53 weakref.finalize(self, pp.raw.FPDF_StructTree_Close, self._structtree) 54 weakref.finalize(self, pp.raw.FPDFLink_CloseWebLinks, self._linkpage) 55 56 @cached_property 57 def label(self) -> str: 58 """The page label.""" 59 return self.pdf.get_page_label(self.index) 60 61 @cached_property 62 def width(self) -> float: 63 """The page width.""" 64 return self.get_width() 65 66 @cached_property 67 def height(self) -> float: 68 """The page height.""" 69 return self.get_height() 70 71 @cached_property 72 def rotation(self) -> int: 73 """The page rotation in degrees.""" 74 return self.get_rotation() 75 76 @cached_property 77 def bbox(self) -> Rectangle: 78 """The page bounding box.""" 79 return Rectangle(*self.get_bbox()) 80 81 @cached_property 82 def char_count(self) -> int: 83 """The total count of characters.""" 84 return self._text.count_chars() 85 86 @cached_property 87 def _chars(self) -> list[Character | None]: 88 return [None] * self.char_count 89 90 @cached_property 91 def _glyph_unicodes(self) -> dict[tuple[str, int], int]: 92 return decode_glyphs(self) 93 94 def char(self, index: int) -> Character: 95 """:return: The character at the 0-index.""" 96 # A list per page is smaller than a global `functools.cache`, which 97 # also keeps every page alive after the document is closed. 98 if (char := self._chars[index]) is None: 99 char = self._chars[index] = Character(self, index) 100 return char 101 102 @property 103 def chars(self) -> Iterator[Character]: 104 """Yields all characters.""" 105 for ii in range(self.char_count): 106 yield self.char(ii) 107 108 @cached_property 109 def objlinks(self) -> list[ObjLink]: 110 """All object links.""" 111 links = [] 112 pos = ctypes.c_int(0) 113 link = pp.raw.FPDF_LINK() 114 while pp.raw.FPDFLink_Enumerate(self, pos, link): 115 links.append(ObjLink(self, link)) 116 return links 117 118 @cached_property 119 def weblinks(self) -> list[WebLink]: 120 """All web links.""" 121 links = [] 122 for ii in range(pp.raw.FPDFLink_CountWebLinks(self._linkpage)): 123 links.append(WebLink(self, ii)) 124 return links 125 126 def chars_in_area(self, area: Rectangle) -> list[Character]: 127 """ 128 :param area: area to search for character in. 129 :return: All characters found in the area. 130 """ 131 found = [] 132 # We perform binary searches of the lower and upper y-positions first 133 # lines are ordered by y-position 134 ypositions = list(self._charlines.keys()) 135 y_bottom = bisect_left(ypositions, area.bottom) 136 y_top = bisect_right(ypositions, area.top, lo=y_bottom) 137 138 # Then for every line we do another binary search for left and right 139 for ypos in ypositions[y_bottom:y_top]: 140 chars = self._charlines[ypos] 141 x_left = bisect_left(chars, area.left, key=lambda c: c.bbox.midpoint.x) 142 x_right = bisect_right(chars, area.right, lo=x_left, key=lambda c: c.bbox.midpoint.x) 143 # Finally we add all these characters 144 found.extend(chars[x_left:x_right]) 145 return found 146 147 def text_in_area(self, area: Rectangle) -> str: 148 """ 149 :param area: area to search for text in. 150 :return: Only the text found in the area. 151 """ 152 return self._text.get_text_bounded(area.left, area.bottom, area.right, area.top) 153 154 @property 155 def structures(self) -> Iterator[Structure]: 156 """The PDF/UA tags.""" 157 count = pp.raw.FPDF_StructTree_CountChildren(self._structtree) 158 for ii in range(count): 159 child = pp.raw.FPDF_StructTree_GetChildAtIndex(self._structtree, ii) 160 yield Structure(self, child) 161 162 def find(self, string: str, case_sensitive: bool = True) -> Iterator[Character]: 163 """ 164 Searches for a match string as whole, consecutive words and yields the 165 characters. 166 167 :param string: The search string. 168 :param case_sensitive: Ignore case if false. 169 :return: yields the characters found. 170 """ 171 searcher = self._text.search(string, match_case=case_sensitive, match_whole_word=True, consecutive=True) 172 while idx := searcher.get_next(): 173 chars = [self.char(ii) for ii in range(idx[0], idx[0] + idx[1])] 174 yield chars 175 176 @cached_property 177 def paths(self) -> list[Path]: 178 """All paths.""" 179 return [Path(o) for o in self.get_objects([pp.raw.FPDF_PAGEOBJ_PATH])] 180 181 @cached_property 182 def images(self) -> list[Image]: 183 """All images.""" 184 return [Image(o) for o in self.get_objects([pp.raw.FPDF_PAGEOBJ_IMAGE])] 185 186 def graphic_clusters( 187 self, predicate: Callable[[Path | Image], bool] = None, absolute_tolerance: float = None 188 ) -> list[tuple[Rectangle, list[Path]]]: 189 if absolute_tolerance is None: 190 absolute_tolerance = min(self.width, self.height) * 0.01 191 192 # First collect all vertical regions 193 filtered_paths = [] 194 for path in self.paths: 195 if predicate is None or predicate(path): 196 filtered_paths.append(path) 197 for image in self.images: 198 if predicate is None or predicate(image): 199 filtered_paths.append(image) 200 201 regions = [] 202 for path in sorted(filtered_paths, key=lambda path: path.bbox.y): 203 for reg in regions: 204 if reg.overlaps(path.bbox.bottom, path.bbox.top, absolute_tolerance): 205 # They overlap, so merge them 206 reg.v0 = min(reg.v0, path.bbox.bottom) 207 reg.v1 = max(reg.v1, path.bbox.top) 208 reg.objs.append(path) 209 break 210 else: 211 regions.append(Region(path.bbox.bottom, path.bbox.top, path)) 212 213 # Now collect horizontal region inside each vertical region 214 for yreg in regions: 215 for path in sorted(filtered_paths, key=lambda path: path.bbox.x): 216 # check if horizontal line is contained in vregion 217 if yreg.contains(path.bbox.y, absolute_tolerance): 218 for xreg in yreg.subregions: 219 if xreg.overlaps(path.bbox.left, path.bbox.right, absolute_tolerance): 220 # They overlap so merge them 221 xreg.v0 = min(xreg.v0, path.bbox.left) 222 xreg.v1 = max(xreg.v1, path.bbox.right) 223 xreg.objs.append(path) 224 break 225 else: 226 yreg.subregions.append(Region(path.bbox.left, path.bbox.right, path)) 227 228 clusters = [] 229 for yreg in regions: 230 for xreg in yreg.subregions: 231 if len(yreg.subregions) > 1: 232 # Strip down the height again for subregions 233 y0, y1 = 1e9, 0 234 for path in xreg.objs: 235 y0 = min(y0, path.bbox.bottom) 236 y1 = max(y1, path.bbox.top) 237 else: 238 y0, y1 = yreg.v0, yreg.v1 239 bbox = Rectangle(xreg.v0, y0, xreg.v1, y1) 240 clusters.append((bbox, xreg.objs)) 241 242 return sorted(clusters, key=lambda c: (-c[0].y, c[0].x)) 243 244 def _link_characters(self): 245 if self._linked: 246 return 247 # The in-document links only gives us rectangles and we must find the 248 # linked chars ourselves 249 for link in self.objlinks: 250 for char in self.chars_in_area(link.bbox): 251 char.objlink = link 252 # The weblinks give you an explicit char range, very convenient 253 for link in self.weblinks: 254 for ii in range(*link.range): 255 self.char(ii).weblink = link 256 self._linked = True 257 258 @cached_property 259 def _charlines(self): 260 charlines = defaultdict(list) 261 for char in self.chars: 262 charlines[round(char.bbox.midpoint.y, 1)].append(char) 263 264 orderedchars = OrderedDict.fromkeys(sorted(charlines)) 265 for ypos, chars in charlines.items(): 266 orderedchars[ypos] = sorted(chars, key=lambda c: c.bbox.midpoint.x) 267 268 return orderedchars
This class provides low-level access to graphics and characters of the page and allows searching for characters in an area instead of just text.
31 def __init__(self, document: "modm_data.pdf.Document", index: int): # noqa: F821 32 """ 33 :param document: a PDF document. 34 :param index: 0-index page number. 35 """ 36 self.index = index 37 """0-index page number.""" 38 self.number = index + 1 39 """1-index page number.""" 40 41 super().__init__(pp.raw.FPDF_LoadPage(document, index), document, document.formenv) 42 document._add_kid(self) 43 self._links = None 44 self._weblinks = None 45 self._linked = False 46 47 _LOGGER.debug(f"Loading: {index}") 48 49 self._text = self.get_textpage() 50 self._linkpage = pp.raw.FPDFLink_LoadWebLinks(self._text) 51 self._structtree = pp.raw.FPDF_StructTree_GetForPage(self) 52 # close them in reverse order 53 weakref.finalize(self, pp.raw.FPDF_StructTree_Close, self._structtree) 54 weakref.finalize(self, pp.raw.FPDFLink_CloseWebLinks, self._linkpage)
Parameters
- document: a PDF document.
- index: 0-index page number.
56 @cached_property 57 def label(self) -> str: 58 """The page label.""" 59 return self.pdf.get_page_label(self.index)
The page label.
61 @cached_property 62 def width(self) -> float: 63 """The page width.""" 64 return self.get_width()
The page width.
66 @cached_property 67 def height(self) -> float: 68 """The page height.""" 69 return self.get_height()
The page height.
71 @cached_property 72 def rotation(self) -> int: 73 """The page rotation in degrees.""" 74 return self.get_rotation()
The page rotation in degrees.
76 @cached_property 77 def bbox(self) -> Rectangle: 78 """The page bounding box.""" 79 return Rectangle(*self.get_bbox())
The page bounding box.
81 @cached_property 82 def char_count(self) -> int: 83 """The total count of characters.""" 84 return self._text.count_chars()
The total count of characters.
94 def char(self, index: int) -> Character: 95 """:return: The character at the 0-index.""" 96 # A list per page is smaller than a global `functools.cache`, which 97 # also keeps every page alive after the document is closed. 98 if (char := self._chars[index]) is None: 99 char = self._chars[index] = Character(self, index) 100 return char
Returns
The character at the 0-index.
102 @property 103 def chars(self) -> Iterator[Character]: 104 """Yields all characters.""" 105 for ii in range(self.char_count): 106 yield self.char(ii)
Yields all characters.
108 @cached_property 109 def objlinks(self) -> list[ObjLink]: 110 """All object links.""" 111 links = [] 112 pos = ctypes.c_int(0) 113 link = pp.raw.FPDF_LINK() 114 while pp.raw.FPDFLink_Enumerate(self, pos, link): 115 links.append(ObjLink(self, link)) 116 return links
All object links.
118 @cached_property 119 def weblinks(self) -> list[WebLink]: 120 """All web links.""" 121 links = [] 122 for ii in range(pp.raw.FPDFLink_CountWebLinks(self._linkpage)): 123 links.append(WebLink(self, ii)) 124 return links
All web links.
126 def chars_in_area(self, area: Rectangle) -> list[Character]: 127 """ 128 :param area: area to search for character in. 129 :return: All characters found in the area. 130 """ 131 found = [] 132 # We perform binary searches of the lower and upper y-positions first 133 # lines are ordered by y-position 134 ypositions = list(self._charlines.keys()) 135 y_bottom = bisect_left(ypositions, area.bottom) 136 y_top = bisect_right(ypositions, area.top, lo=y_bottom) 137 138 # Then for every line we do another binary search for left and right 139 for ypos in ypositions[y_bottom:y_top]: 140 chars = self._charlines[ypos] 141 x_left = bisect_left(chars, area.left, key=lambda c: c.bbox.midpoint.x) 142 x_right = bisect_right(chars, area.right, lo=x_left, key=lambda c: c.bbox.midpoint.x) 143 # Finally we add all these characters 144 found.extend(chars[x_left:x_right]) 145 return found
Parameters
- area: area to search for character in.
Returns
All characters found in the area.
147 def text_in_area(self, area: Rectangle) -> str: 148 """ 149 :param area: area to search for text in. 150 :return: Only the text found in the area. 151 """ 152 return self._text.get_text_bounded(area.left, area.bottom, area.right, area.top)
Parameters
- area: area to search for text in.
Returns
Only the text found in the area.
154 @property 155 def structures(self) -> Iterator[Structure]: 156 """The PDF/UA tags.""" 157 count = pp.raw.FPDF_StructTree_CountChildren(self._structtree) 158 for ii in range(count): 159 child = pp.raw.FPDF_StructTree_GetChildAtIndex(self._structtree, ii) 160 yield Structure(self, child)
The PDF/UA tags.
162 def find(self, string: str, case_sensitive: bool = True) -> Iterator[Character]: 163 """ 164 Searches for a match string as whole, consecutive words and yields the 165 characters. 166 167 :param string: The search string. 168 :param case_sensitive: Ignore case if false. 169 :return: yields the characters found. 170 """ 171 searcher = self._text.search(string, match_case=case_sensitive, match_whole_word=True, consecutive=True) 172 while idx := searcher.get_next(): 173 chars = [self.char(ii) for ii in range(idx[0], idx[0] + idx[1])] 174 yield chars
Searches for a match string as whole, consecutive words and yields the characters.
Parameters
- string: The search string.
- case_sensitive: Ignore case if false.
Returns
yields the characters found.
176 @cached_property 177 def paths(self) -> list[Path]: 178 """All paths.""" 179 return [Path(o) for o in self.get_objects([pp.raw.FPDF_PAGEOBJ_PATH])]
All paths.
181 @cached_property 182 def images(self) -> list[Image]: 183 """All images.""" 184 return [Image(o) for o in self.get_objects([pp.raw.FPDF_PAGEOBJ_IMAGE])]
All images.
186 def graphic_clusters( 187 self, predicate: Callable[[Path | Image], bool] = None, absolute_tolerance: float = None 188 ) -> list[tuple[Rectangle, list[Path]]]: 189 if absolute_tolerance is None: 190 absolute_tolerance = min(self.width, self.height) * 0.01 191 192 # First collect all vertical regions 193 filtered_paths = [] 194 for path in self.paths: 195 if predicate is None or predicate(path): 196 filtered_paths.append(path) 197 for image in self.images: 198 if predicate is None or predicate(image): 199 filtered_paths.append(image) 200 201 regions = [] 202 for path in sorted(filtered_paths, key=lambda path: path.bbox.y): 203 for reg in regions: 204 if reg.overlaps(path.bbox.bottom, path.bbox.top, absolute_tolerance): 205 # They overlap, so merge them 206 reg.v0 = min(reg.v0, path.bbox.bottom) 207 reg.v1 = max(reg.v1, path.bbox.top) 208 reg.objs.append(path) 209 break 210 else: 211 regions.append(Region(path.bbox.bottom, path.bbox.top, path)) 212 213 # Now collect horizontal region inside each vertical region 214 for yreg in regions: 215 for path in sorted(filtered_paths, key=lambda path: path.bbox.x): 216 # check if horizontal line is contained in vregion 217 if yreg.contains(path.bbox.y, absolute_tolerance): 218 for xreg in yreg.subregions: 219 if xreg.overlaps(path.bbox.left, path.bbox.right, absolute_tolerance): 220 # They overlap so merge them 221 xreg.v0 = min(xreg.v0, path.bbox.left) 222 xreg.v1 = max(xreg.v1, path.bbox.right) 223 xreg.objs.append(path) 224 break 225 else: 226 yreg.subregions.append(Region(path.bbox.left, path.bbox.right, path)) 227 228 clusters = [] 229 for yreg in regions: 230 for xreg in yreg.subregions: 231 if len(yreg.subregions) > 1: 232 # Strip down the height again for subregions 233 y0, y1 = 1e9, 0 234 for path in xreg.objs: 235 y0 = min(y0, path.bbox.bottom) 236 y1 = max(y1, path.bbox.top) 237 else: 238 y0, y1 = yreg.v0, yreg.v1 239 bbox = Rectangle(xreg.v0, y0, xreg.v1, y1) 240 clusters.append((bbox, xreg.objs)) 241 242 return sorted(clusters, key=lambda c: (-c[0].y, c[0].x))
Inherited Members
- pypdfium2._helpers.page.PdfPage
- raw
- formenv
- parent
- get_width
- get_height
- get_size
- get_rotation
- set_rotation
- get_mediabox
- set_mediabox
- get_cropbox
- set_cropbox
- get_bleedbox
- set_bleedbox
- get_trimbox
- set_trimbox
- get_artbox
- set_artbox
- get_bbox
- get_textpage
- insert_obj
- remove_obj
- gen_content
- get_objects
- flatten
- render
- pypdfium2.internal.bases.AutoCloseable
- close
37class Character: 38 """ 39 Each character on the PDF page is represented by a character object, 40 describing exactly where and how to render the associated glyph. 41 42 While there are font flags, PDF files typically use entirely different fonts 43 to render normal, bold, and italic characters. 44 45 The character's loose bounding box may not always be available, since it 46 must be explicitly provided by the font. The tight bounding box is only 47 available as long as the glyph is renderable, so a space character may have 48 a loose, but not a tight bounding box, or none at all. 49 """ 50 51 class RenderMode(Enum): 52 """Tells the PDF viewer how to render this character glyph.""" 53 54 UNKNOWN = -1 55 FILL = 0 56 STROKE = 1 57 FILL_STROKE = 2 58 INVISIBLE = 3 59 FILL_CLIP = 4 60 STROKE_CLIP = 5 61 FILL_STROKE_CLIP = 6 62 CLIP = 7 63 64 # A dictionary per character is the largest part of the memory usage 65 __slots__ = ( 66 "_bbox", 67 "_fill_", 68 "_font", 69 "_index", 70 "_origin_", 71 "_page", 72 "_render_mode_", 73 "_rotation", 74 "_size_", 75 "_stroke_", 76 "_tbbox_", 77 "_text", 78 "_weight_", 79 "objlink", 80 "unicode", 81 "weblink", 82 ) 83 84 def __init__(self, page: "modm_data.pdf.page.Page", index: int): # noqa: F821 85 """ 86 :param page: The page containing the character. 87 :param index: The index of the character. 88 """ 89 self._page = page 90 self._text = page._text 91 self._index = index 92 self._font = None 93 self._rotation = int(math.degrees(pp.raw.FPDFText_GetCharAngle(self._text, self._index))) 94 95 self.unicode: int = pp.raw.FPDFText_GetUnicode(self._text, self._index) 96 """The unicode value of the character.""" 97 if pp.raw.FPDFText_HasUnicodeMapError(self._text, self._index): 98 self.unicode = self._page._glyph_unicodes.get((self.font, self.unicode), self.unicode) 99 if 0xD800 <= self.unicode < 0xDC00: 100 # Characters outside the BMP are split into two UTF-16 code units, 101 # so we combine them into the first and ignore the second one. 102 low = pp.raw.FPDFText_GetUnicode(self._text, self._index + 1) 103 if 0xDC00 <= low < 0xE000: 104 self.unicode = 0x10000 + ((self.unicode - 0xD800) << 10) + low - 0xDC00 105 elif 0xDC00 <= self.unicode < 0xE000: 106 self.unicode = 0 107 self.objlink: "modm_data.pdf.link.ObjLink" = None # noqa: F821 108 """The object link of this character or `None`""" 109 self.weblink: "modm_data.pdf.link.WebLink" = None # noqa: F821 110 """The web link of this character or `None`""" 111 112 bbox = Rectangle(*self._em_box()) 113 if self._page.rotation: 114 bbox = Rectangle(bbox.p0.y, self._page.height - bbox.p1.x, bbox.p1.y, self._page.height - bbox.p0.x) 115 self._bbox = bbox 116 117 def _em_box(self) -> tuple[float, float, float, float]: 118 # The layout heuristics need the font size and the advance width of a 119 # character, not the extent of its glyph. We therefore compute our own 120 # box, which is exactly one font size tall, one advance width wide and 121 # placed by the text matrix, so that it also works for rotated text. 122 # The loose box of pdfium is not suitable, since its definition changed 123 # between versions and its height depends on the font metrics. 124 tight = self._text.get_charbox(self._index) 125 obj = pp.raw.FPDFText_GetTextObject(self._text, self._index) 126 size = pp.raw.FPDFText_GetFontSize(self._text, self._index) 127 if not obj or not size or pp.raw.FPDFText_IsGenerated(self._text, self._index): 128 return tight 129 font = pp.raw.FPDFTextObj_GetFont(obj) 130 ascent, descent, width = ctypes.c_float(), ctypes.c_float(), ctypes.c_float() 131 pp.raw.FPDFFont_GetAscent(font, size, ascent) 132 pp.raw.FPDFFont_GetDescent(font, size, descent) 133 if ascent.value == descent.value: 134 return tight 135 136 m = pp.raw.FS_MATRIX() 137 ox, oy = ctypes.c_double(), ctypes.c_double() 138 assert pp.raw.FPDFText_GetMatrix(self._text, self._index, m) 139 assert pp.raw.FPDFText_GetCharOrigin(self._text, self._index, ox, oy) 140 ox, oy = ox.value, oy.value 141 142 # Split the font size above and below the baseline like the font does 143 scale = size / (ascent.value - descent.value) 144 y0, y1 = descent.value * scale, ascent.value * scale 145 x1 = 0 146 if pp.raw.FPDFFont_GetGlyphWidth(font, self.unicode, size, width): 147 x1 = width.value 148 if not x1 and (norm := m.a * m.a + m.b * m.b): 149 # The width lookup fails for characters without a reverse unicode 150 # mapping, so we use the extent of the glyph along the baseline. 151 x1 = max(((x - ox) * m.a + (y - oy) * m.b) / norm for x in tight[::2] for y in tight[1::2]) 152 153 xs = [ox + m.a * x + m.c * y for x in (0, x1) for y in (y0, y1)] 154 ys = [oy + m.b * x + m.d * y for x in (0, x1) for y in (y0, y1)] 155 return min(xs), min(ys), max(xs), max(ys) 156 157 def generated_space(self) -> "Character": 158 """ 159 :return: A zero-sized space character located at the origin of this 160 character, like the ones pdfium generates for visual gaps. 161 """ 162 space = copy.copy(self) 163 space.unicode = 0x20 164 space._bbox = space.tbbox = Rectangle(self.origin.x, self.origin.y, self.origin.x, self.origin.y) 165 space.origin = self.origin 166 return space 167 168 def _font_flags(self) -> tuple[str, int]: 169 if self._font is None: 170 font = ctypes.create_string_buffer(255) 171 flags = ctypes.c_int() 172 pp.raw.FPDFText_GetFontInfo(self._text, self._index, font, 255, flags) 173 # Share one tuple per font instead of one per character 174 font = (font.value.decode("utf-8", errors="replace"), flags.value) 175 self._font = _FONTS.setdefault(font, font) 176 return self._font 177 178 @property 179 def char(self) -> str: 180 """The printable string of the unicode value.""" 181 char = chr(self.unicode) 182 return char if char.isprintable() else "" 183 184 @_slot_cache 185 def origin(self) -> Point: 186 """The origin of the character.""" 187 x, y = ctypes.c_double(), ctypes.c_double() 188 assert pp.raw.FPDFText_GetCharOrigin(self._text, self._index, x, y) 189 if self._page.rotation: 190 return Point(y.value, self._page.height - x.value) 191 return Point(x.value, y.value) 192 193 @property 194 def width(self) -> float: 195 """The width of the character's bounding box.""" 196 if self.rotation: 197 return self.bbox.height 198 return self.bbox.width 199 200 @property 201 def height(self) -> float: 202 """The height of the character's bounding box.""" 203 if self.rotation: 204 return self.bbox.width 205 return self.bbox.height 206 207 @_slot_cache 208 def tbbox(self) -> Rectangle: 209 """The tight bounding box of the character.""" 210 tbbox = Rectangle(*self._text.get_charbox(self._index)) 211 if self._page.rotation: 212 tbbox = Rectangle(tbbox.p0.y, self._page.height - tbbox.p1.x, tbbox.p1.y, self._page.height - tbbox.p0.x) 213 return tbbox 214 215 @property 216 def bbox(self) -> Rectangle: 217 """ 218 The loose bounding box of the character. 219 .. note:: 220 If the loose bounding box is not available, the tight bounding box 221 is used instead. 222 """ 223 if not self._bbox.width or not self._bbox.height: 224 return self.tbbox 225 return self._bbox 226 227 @property 228 def twidth(self) -> float: 229 """The width of the character's tight bounding box.""" 230 return self.tbbox.width 231 232 @property 233 def theight(self) -> float: 234 """The height of the character's tight bounding box.""" 235 return self.tbbox.height 236 237 @_slot_cache 238 def render_mode(self) -> RenderMode: 239 """The render mode of the character.""" 240 return Character.RenderMode( 241 pp.raw.FPDFTextObj_GetTextRenderMode(pp.raw.FPDFText_GetTextObject(self._text, self._index)) 242 ) 243 244 @property 245 def rotation(self) -> int: 246 """The rotation of the character in degrees modulo 360.""" 247 # Special case for vertical text in rotated pages 248 if self._page.rotation == 90 and self._rotation == 0 and self.unicode not in {0x20, 0xA, 0xD}: 249 return 90 250 if self._page.rotation and self._rotation: 251 return (self._page.rotation + self._rotation) % 360 252 return self._rotation 253 254 @_slot_cache 255 def size(self) -> float: 256 """The font size of the character.""" 257 return pp.raw.FPDFText_GetFontSize(self._text, self._index) 258 259 @_slot_cache 260 def weight(self) -> int: 261 """The font weight of the character.""" 262 return pp.raw.FPDFText_GetFontWeight(self._text, self._index) 263 264 @_slot_cache 265 def fill(self) -> int: 266 """The fill color of the character.""" 267 r, g, b, a = ctypes.c_uint(), ctypes.c_uint(), ctypes.c_uint(), ctypes.c_uint() 268 pp.raw.FPDFText_GetFillColor(self._text, self._index, r, g, b, a) 269 return r.value << 24 | g.value << 16 | b.value << 8 | a.value 270 271 @_slot_cache 272 def stroke(self) -> int: 273 """The stroke color of the character.""" 274 r, g, b, a = ctypes.c_uint(), ctypes.c_uint(), ctypes.c_uint(), ctypes.c_uint() 275 pp.raw.FPDFText_GetStrokeColor(self._text, self._index, r, g, b, a) 276 return r.value << 24 | g.value << 16 | b.value << 8 | a.value 277 278 @property 279 def font(self) -> str: 280 """The font name of the character.""" 281 return self._font_flags()[0] 282 283 @property 284 def flags(self) -> int: 285 """The font flags of the character.""" 286 return self._font_flags()[1] 287 288 @property 289 def has_space_glyph(self) -> bool: 290 """ 291 Whether the font of the character has a space with a width. pdfium 292 cannot generate spaces at the visual gaps of text in fonts without. 293 """ 294 obj = pp.raw.FPDFText_GetTextObject(self._text, self._index) 295 if not obj: 296 return True 297 width = ctypes.c_float() 298 font = pp.raw.FPDFTextObj_GetFont(obj) 299 return not pp.raw.FPDFFont_GetGlyphWidth(font, 0x20, 1, width) or width.value > 0 300 301 def descr(self) -> str: 302 """Human-readable description of the character for debugging.""" 303 char = chr(self.unicode) 304 if not char.isprintable(): 305 char = hex(self.unicode) 306 return ( 307 f"Chr({char}, {self.size}, {self.weight}, {self.rotation}, " 308 f"{self.render_mode}, {self.font}, {hex(self.flags)}, " 309 f"{self.fill}, {self.stroke}, {repr(self.bbox)})" 310 ) 311 312 def __str__(self) -> str: 313 return self.char 314 315 def __repr__(self) -> str: 316 char = chr(self.unicode) 317 escape = {0xA: "\\n", 0xD: "\\r", 0x9: "\\t", 0x20: "␣"} 318 char = escape.get(self.unicode, char if char.isprintable() else hex(self.unicode)) 319 return char
Each character on the PDF page is represented by a character object, describing exactly where and how to render the associated glyph.
While there are font flags, PDF files typically use entirely different fonts to render normal, bold, and italic characters.
The character's loose bounding box may not always be available, since it must be explicitly provided by the font. The tight bounding box is only available as long as the glyph is renderable, so a space character may have a loose, but not a tight bounding box, or none at all.
84 def __init__(self, page: "modm_data.pdf.page.Page", index: int): # noqa: F821 85 """ 86 :param page: The page containing the character. 87 :param index: The index of the character. 88 """ 89 self._page = page 90 self._text = page._text 91 self._index = index 92 self._font = None 93 self._rotation = int(math.degrees(pp.raw.FPDFText_GetCharAngle(self._text, self._index))) 94 95 self.unicode: int = pp.raw.FPDFText_GetUnicode(self._text, self._index) 96 """The unicode value of the character.""" 97 if pp.raw.FPDFText_HasUnicodeMapError(self._text, self._index): 98 self.unicode = self._page._glyph_unicodes.get((self.font, self.unicode), self.unicode) 99 if 0xD800 <= self.unicode < 0xDC00: 100 # Characters outside the BMP are split into two UTF-16 code units, 101 # so we combine them into the first and ignore the second one. 102 low = pp.raw.FPDFText_GetUnicode(self._text, self._index + 1) 103 if 0xDC00 <= low < 0xE000: 104 self.unicode = 0x10000 + ((self.unicode - 0xD800) << 10) + low - 0xDC00 105 elif 0xDC00 <= self.unicode < 0xE000: 106 self.unicode = 0 107 self.objlink: "modm_data.pdf.link.ObjLink" = None # noqa: F821 108 """The object link of this character or `None`""" 109 self.weblink: "modm_data.pdf.link.WebLink" = None # noqa: F821 110 """The web link of this character or `None`""" 111 112 bbox = Rectangle(*self._em_box()) 113 if self._page.rotation: 114 bbox = Rectangle(bbox.p0.y, self._page.height - bbox.p1.x, bbox.p1.y, self._page.height - bbox.p0.x) 115 self._bbox = bbox
Parameters
- page: The page containing the character.
- index: The index of the character.
157 def generated_space(self) -> "Character": 158 """ 159 :return: A zero-sized space character located at the origin of this 160 character, like the ones pdfium generates for visual gaps. 161 """ 162 space = copy.copy(self) 163 space.unicode = 0x20 164 space._bbox = space.tbbox = Rectangle(self.origin.x, self.origin.y, self.origin.x, self.origin.y) 165 space.origin = self.origin 166 return space
Returns
A zero-sized space character located at the origin of this character, like the ones pdfium generates for visual gaps.
178 @property 179 def char(self) -> str: 180 """The printable string of the unicode value.""" 181 char = chr(self.unicode) 182 return char if char.isprintable() else ""
The printable string of the unicode value.
193 @property 194 def width(self) -> float: 195 """The width of the character's bounding box.""" 196 if self.rotation: 197 return self.bbox.height 198 return self.bbox.width
The width of the character's bounding box.
200 @property 201 def height(self) -> float: 202 """The height of the character's bounding box.""" 203 if self.rotation: 204 return self.bbox.width 205 return self.bbox.height
The height of the character's bounding box.
215 @property 216 def bbox(self) -> Rectangle: 217 """ 218 The loose bounding box of the character. 219 .. note:: 220 If the loose bounding box is not available, the tight bounding box 221 is used instead. 222 """ 223 if not self._bbox.width or not self._bbox.height: 224 return self.tbbox 225 return self._bbox
The loose bounding box of the character.
If the loose bounding box is not available, the tight bounding box is used instead.
227 @property 228 def twidth(self) -> float: 229 """The width of the character's tight bounding box.""" 230 return self.tbbox.width
The width of the character's tight bounding box.
232 @property 233 def theight(self) -> float: 234 """The height of the character's tight bounding box.""" 235 return self.tbbox.height
The height of the character's tight bounding box.
244 @property 245 def rotation(self) -> int: 246 """The rotation of the character in degrees modulo 360.""" 247 # Special case for vertical text in rotated pages 248 if self._page.rotation == 90 and self._rotation == 0 and self.unicode not in {0x20, 0xA, 0xD}: 249 return 90 250 if self._page.rotation and self._rotation: 251 return (self._page.rotation + self._rotation) % 360 252 return self._rotation
The rotation of the character in degrees modulo 360.
278 @property 279 def font(self) -> str: 280 """The font name of the character.""" 281 return self._font_flags()[0]
The font name of the character.
283 @property 284 def flags(self) -> int: 285 """The font flags of the character.""" 286 return self._font_flags()[1]
The font flags of the character.
288 @property 289 def has_space_glyph(self) -> bool: 290 """ 291 Whether the font of the character has a space with a width. pdfium 292 cannot generate spaces at the visual gaps of text in fonts without. 293 """ 294 obj = pp.raw.FPDFText_GetTextObject(self._text, self._index) 295 if not obj: 296 return True 297 width = ctypes.c_float() 298 font = pp.raw.FPDFTextObj_GetFont(obj) 299 return not pp.raw.FPDFFont_GetGlyphWidth(font, 0x20, 1, width) or width.value > 0
Whether the font of the character has a space with a width. pdfium cannot generate spaces at the visual gaps of text in fonts without.
301 def descr(self) -> str: 302 """Human-readable description of the character for debugging.""" 303 char = chr(self.unicode) 304 if not char.isprintable(): 305 char = hex(self.unicode) 306 return ( 307 f"Chr({char}, {self.size}, {self.weight}, {self.rotation}, " 308 f"{self.render_mode}, {self.font}, {hex(self.flags)}, " 309 f"{self.fill}, {self.stroke}, {repr(self.bbox)})" 310 )
Human-readable description of the character for debugging.
51 class RenderMode(Enum): 52 """Tells the PDF viewer how to render this character glyph.""" 53 54 UNKNOWN = -1 55 FILL = 0 56 STROKE = 1 57 FILL_STROKE = 2 58 INVISIBLE = 3 59 FILL_CLIP = 4 60 STROKE_CLIP = 5 61 FILL_STROKE_CLIP = 6 62 CLIP = 7
Tells the PDF viewer how to render this character glyph.
Inherited Members
- enum.Enum
- name
- value
12class Path(pp.PdfObject): 13 """ 14 PDF uses a subset of the PostScript graphics language, which draws vector 15 paths with various rendering options. We are only interested in the basic 16 properties, in particular, for recognizing table cell borders. 17 18 This class specializes `pypdfium2.PdfObject` to add accessors for graphics 19 containing vector paths of various configurations. 20 21 You must construct the paths by calling `modm_data.pdf.page.Page.paths`. 22 """ 23 24 class Type(Enum): 25 """Path Type""" 26 27 LINE = 0 28 BEZIER = 1 29 MOVE = 2 30 31 class Cap(Enum): 32 """Path Cap Type""" 33 34 BUTT = 0 35 ROUND = 1 36 PROJECTING_SQUARE = 2 37 38 class Join(Enum): 39 """Path Join Type""" 40 41 MITER = 0 42 ROUND = 1 43 BEVEL = 2 44 45 # Overwrite the PdfPageObject.__new__ function 46 def __new__(cls, *args, **kwargs): 47 return object.__new__(cls) 48 49 def __init__(self, obj): 50 """ 51 :param obj: PDF object of the path. 52 """ 53 super().__init__(obj.raw, obj.page, obj.pdf, obj.container, obj.level) 54 assert pp.raw.FPDFPageObj_GetType(obj.raw) == pp.raw.FPDF_PAGEOBJ_PATH 55 self.type = pp.raw.FPDF_PAGEOBJ_PATH 56 57 @cached_property 58 def matrix(self) -> pp.PdfMatrix: 59 """The transformation matrix.""" 60 return self.get_matrix() 61 62 @cached_property 63 def count(self) -> int: 64 """Number of segments in this path.""" 65 return pp.raw.FPDFPath_CountSegments(self) 66 67 @cached_property 68 def fill(self) -> int: 69 """The fill color encoded as 32-bit RGBA.""" 70 r, g, b, a = ctypes.c_uint(), ctypes.c_uint(), ctypes.c_uint(), ctypes.c_uint() 71 assert pp.raw.FPDFPageObj_GetFillColor(self, r, g, b, a) 72 return r.value << 24 | g.value << 16 | b.value << 8 | a.value 73 74 @cached_property 75 def stroke(self) -> int: 76 """The stroke color encoded as 32-bit RGBA.""" 77 r, g, b, a = ctypes.c_uint(), ctypes.c_uint(), ctypes.c_uint(), ctypes.c_uint() 78 assert pp.raw.FPDFPageObj_GetStrokeColor(self, r, g, b, a) 79 return r.value << 24 | g.value << 16 | b.value << 8 | a.value 80 81 @cached_property 82 def width(self) -> float: 83 """The stroke width.""" 84 width = ctypes.c_float() 85 assert pp.raw.FPDFPageObj_GetStrokeWidth(self, width) 86 return width.value 87 88 @cached_property 89 def draw_mode(self) -> tuple[int, bool]: 90 """ 91 The fill mode (0: not filled, 1: even-odd rule, 2: non-zero winding 92 rule) and whether the path is stroked. 93 """ 94 fill, stroke = ctypes.c_int(), ctypes.c_int() 95 assert pp.raw.FPDFPath_GetDrawMode(self, fill, stroke) 96 return fill.value, bool(stroke.value) 97 98 @cached_property 99 def dashes(self) -> list[float]: 100 """The stroke dash pattern or an empty list for a solid stroke.""" 101 count = pp.raw.FPDFPageObj_GetDashCount(self) 102 if count <= 0: 103 return [] 104 dashes = (ctypes.c_float * count)() 105 assert pp.raw.FPDFPageObj_GetDashArray(self, dashes, count) 106 return list(dashes) 107 108 @cached_property 109 def cap(self) -> Cap: 110 """Line cap type.""" 111 return Path.Cap(pp.raw.FPDFPageObj_GetLineCap(self)) 112 113 @cached_property 114 def join(self) -> Join: 115 """Line join type.""" 116 return Path.Join(pp.raw.FPDFPageObj_GetLineJoin(self)) 117 118 @cached_property 119 def bbox(self) -> Rectangle: 120 """ 121 Bounding box of the path. 122 .. warning:: 123 The bounding is only approximated using the control points! 124 Therefore bezier curves will likely have a larger bounding box. 125 """ 126 left, bottom = ctypes.c_float(), ctypes.c_float() 127 right, top = ctypes.c_float(), ctypes.c_float() 128 assert pp.raw.FPDFPageObj_GetBounds(self, left, bottom, right, top) 129 bbox = Rectangle(left.value, bottom.value, right.value, top.value) 130 if self.page.rotation: 131 bbox = Rectangle(bbox.p0.y, self.page.height - bbox.p1.x, bbox.p1.y, self.page.height - bbox.p0.x) 132 return bbox 133 134 @cached_property 135 def points(self) -> list[Point]: 136 """ 137 List of points of the path. If the path is closed, the first point is 138 added to the end of the list. 139 """ 140 points = [] 141 for ii in range(self.count): 142 seg = pp.raw.FPDFPath_GetPathSegment(self, ii) 143 ptype = Path.Type(pp.raw.FPDFPathSegment_GetType(seg)) 144 # The first point should always be MOVETO 145 assert ii or ptype == Path.Type.MOVE 146 147 x, y = ctypes.c_float(), ctypes.c_float() 148 assert pp.raw.FPDFPathSegment_GetPoint(seg, x, y) 149 x, y = self.matrix.on_point(x.value, y.value) 150 points.append(Point(x, y, type=ptype)) 151 152 if ptype == Path.Type.MOVE: 153 start = points[-1] 154 if pp.raw.FPDFPathSegment_GetClose(seg): 155 # Closing a path draws a line to the start of the current subpath 156 points.append(Point(start.x, start.y, type=Path.Type.LINE)) 157 158 if self.page.rotation: 159 points = [Point(y, self.page.height - x, type=p.type) for p in points] 160 return points 161 162 @cached_property 163 def lines(self) -> list[Line]: 164 """List of lines between the path points.""" 165 points = self.points 166 return [ 167 Line(points[ii], points[ii + 1], width=self.width, type=points[ii + 1].type) 168 for ii in range(len(points) - 1) 169 ] 170 171 def __repr__(self) -> str: 172 points = ",".join(repr(p) for p in self.points) 173 return f"P{self.count}={points}"
PDF uses a subset of the PostScript graphics language, which draws vector paths with various rendering options. We are only interested in the basic properties, in particular, for recognizing table cell borders.
This class specializes pypdfium2.PdfObject to add accessors for graphics
containing vector paths of various configurations.
You must construct the paths by calling modm_data.pdf.page.Page.paths.
49 def __init__(self, obj): 50 """ 51 :param obj: PDF object of the path. 52 """ 53 super().__init__(obj.raw, obj.page, obj.pdf, obj.container, obj.level) 54 assert pp.raw.FPDFPageObj_GetType(obj.raw) == pp.raw.FPDF_PAGEOBJ_PATH 55 self.type = pp.raw.FPDF_PAGEOBJ_PATH
Parameters
- obj: PDF object of the path.
57 @cached_property 58 def matrix(self) -> pp.PdfMatrix: 59 """The transformation matrix.""" 60 return self.get_matrix()
The transformation matrix.
62 @cached_property 63 def count(self) -> int: 64 """Number of segments in this path.""" 65 return pp.raw.FPDFPath_CountSegments(self)
Number of segments in this path.
67 @cached_property 68 def fill(self) -> int: 69 """The fill color encoded as 32-bit RGBA.""" 70 r, g, b, a = ctypes.c_uint(), ctypes.c_uint(), ctypes.c_uint(), ctypes.c_uint() 71 assert pp.raw.FPDFPageObj_GetFillColor(self, r, g, b, a) 72 return r.value << 24 | g.value << 16 | b.value << 8 | a.value
The fill color encoded as 32-bit RGBA.
74 @cached_property 75 def stroke(self) -> int: 76 """The stroke color encoded as 32-bit RGBA.""" 77 r, g, b, a = ctypes.c_uint(), ctypes.c_uint(), ctypes.c_uint(), ctypes.c_uint() 78 assert pp.raw.FPDFPageObj_GetStrokeColor(self, r, g, b, a) 79 return r.value << 24 | g.value << 16 | b.value << 8 | a.value
The stroke color encoded as 32-bit RGBA.
81 @cached_property 82 def width(self) -> float: 83 """The stroke width.""" 84 width = ctypes.c_float() 85 assert pp.raw.FPDFPageObj_GetStrokeWidth(self, width) 86 return width.value
The stroke width.
88 @cached_property 89 def draw_mode(self) -> tuple[int, bool]: 90 """ 91 The fill mode (0: not filled, 1: even-odd rule, 2: non-zero winding 92 rule) and whether the path is stroked. 93 """ 94 fill, stroke = ctypes.c_int(), ctypes.c_int() 95 assert pp.raw.FPDFPath_GetDrawMode(self, fill, stroke) 96 return fill.value, bool(stroke.value)
The fill mode (0: not filled, 1: even-odd rule, 2: non-zero winding rule) and whether the path is stroked.
98 @cached_property 99 def dashes(self) -> list[float]: 100 """The stroke dash pattern or an empty list for a solid stroke.""" 101 count = pp.raw.FPDFPageObj_GetDashCount(self) 102 if count <= 0: 103 return [] 104 dashes = (ctypes.c_float * count)() 105 assert pp.raw.FPDFPageObj_GetDashArray(self, dashes, count) 106 return list(dashes)
The stroke dash pattern or an empty list for a solid stroke.
108 @cached_property 109 def cap(self) -> Cap: 110 """Line cap type.""" 111 return Path.Cap(pp.raw.FPDFPageObj_GetLineCap(self))
Line cap type.
113 @cached_property 114 def join(self) -> Join: 115 """Line join type.""" 116 return Path.Join(pp.raw.FPDFPageObj_GetLineJoin(self))
Line join type.
118 @cached_property 119 def bbox(self) -> Rectangle: 120 """ 121 Bounding box of the path. 122 .. warning:: 123 The bounding is only approximated using the control points! 124 Therefore bezier curves will likely have a larger bounding box. 125 """ 126 left, bottom = ctypes.c_float(), ctypes.c_float() 127 right, top = ctypes.c_float(), ctypes.c_float() 128 assert pp.raw.FPDFPageObj_GetBounds(self, left, bottom, right, top) 129 bbox = Rectangle(left.value, bottom.value, right.value, top.value) 130 if self.page.rotation: 131 bbox = Rectangle(bbox.p0.y, self.page.height - bbox.p1.x, bbox.p1.y, self.page.height - bbox.p0.x) 132 return bbox
Bounding box of the path.
The bounding is only approximated using the control points! Therefore bezier curves will likely have a larger bounding box.
134 @cached_property 135 def points(self) -> list[Point]: 136 """ 137 List of points of the path. If the path is closed, the first point is 138 added to the end of the list. 139 """ 140 points = [] 141 for ii in range(self.count): 142 seg = pp.raw.FPDFPath_GetPathSegment(self, ii) 143 ptype = Path.Type(pp.raw.FPDFPathSegment_GetType(seg)) 144 # The first point should always be MOVETO 145 assert ii or ptype == Path.Type.MOVE 146 147 x, y = ctypes.c_float(), ctypes.c_float() 148 assert pp.raw.FPDFPathSegment_GetPoint(seg, x, y) 149 x, y = self.matrix.on_point(x.value, y.value) 150 points.append(Point(x, y, type=ptype)) 151 152 if ptype == Path.Type.MOVE: 153 start = points[-1] 154 if pp.raw.FPDFPathSegment_GetClose(seg): 155 # Closing a path draws a line to the start of the current subpath 156 points.append(Point(start.x, start.y, type=Path.Type.LINE)) 157 158 if self.page.rotation: 159 points = [Point(y, self.page.height - x, type=p.type) for p in points] 160 return points
List of points of the path. If the path is closed, the first point is added to the end of the list.
162 @cached_property 163 def lines(self) -> list[Line]: 164 """List of lines between the path points.""" 165 points = self.points 166 return [ 167 Line(points[ii], points[ii + 1], width=self.width, type=points[ii + 1].type) 168 for ii in range(len(points) - 1) 169 ]
List of lines between the path points.
Inherited Members
- pypdfium2._helpers.pageobjects.PdfObject
- raw
- page
- container
- level
- parent
- get_bounds
- get_quad_points
- get_matrix
- set_matrix
- transform
- pypdfium2.internal.bases.AutoCloseable
- close
Path Type
Inherited Members
- enum.Enum
- name
- value
Path Cap Type
Inherited Members
- enum.Enum
- name
- value
Path Join Type
Inherited Members
- enum.Enum
- name
- value
12class Image(pp.PdfImage): 13 """ 14 This class extends `pypdfium2.PdfImage` to align it with the interface of 15 the `Path` class so that it can be used in the same 16 algorithms without filtering. 17 18 You must construct the images by calling `modm_data.pdf.page.Page.images`. 19 20 .. note:: Images are currently ignored. 21 """ 22 23 # Overwrite the PdfPageObject.__new__ function 24 def __new__(cls, *args, **kwargs): 25 return object.__new__(cls) 26 27 def __init__(self, obj): 28 """ 29 :param obj: Page object of the image. 30 """ 31 super().__init__(obj.raw, obj.page, obj.pdf, obj.container, obj.level) 32 assert pp.raw.FPDFPageObj_GetType(obj.raw) == pp.raw.FPDF_PAGEOBJ_IMAGE 33 self.type = pp.raw.FPDF_PAGEOBJ_IMAGE 34 35 self.count: int = 4 36 """Number of segments. Always 4 due to rectangular image form. 37 (For compatibility with `Path.count`.)""" 38 self.stroke: int = 0 39 """The border stroke color. Always 0. 40 (For compatibility with `Path.stroke`.)""" 41 self.fill: int = 0 42 """The image fill color. Always 0. 43 (For compatibility with `Path.fill`.)""" 44 self.width: float = 0 45 """The border line width. Always 0. 46 (For compatibility with `Path.width`.)""" 47 48 @cached_property 49 def matrix(self) -> pp.PdfMatrix: 50 """The transformation matrix.""" 51 return self.get_matrix() 52 53 @cached_property 54 def bbox(self) -> Rectangle: 55 """The bounding box of the image.""" 56 bbox = Rectangle(*self.get_bounds()) 57 if self.page.rotation: 58 bbox = Rectangle(bbox.p0.y, self.page.height - bbox.p1.x, bbox.p1.y, self.page.height - bbox.p0.x) 59 return bbox 60 61 @cached_property 62 def points(self) -> list[Point]: 63 """ 64 The 4 points of the bounding box. 65 (For compatibility with `Path.points`.) 66 """ 67 points = self.bbox.points 68 if self.page.rotation: 69 points = [Point(p.y, self.page.height - p.x, p.type) for p in points] 70 return points 71 72 @cached_property 73 def lines(self) -> list[Line]: 74 """ 75 The 4 lines of the bounding box. 76 (For compatibility with `Path.lines`.) 77 """ 78 p = self.points 79 return [ 80 Line(p[0], p[1], p[1].type, 0), 81 Line(p[1], p[2], p[2].type, 0), 82 Line(p[2], p[3], p[3].type, 0), 83 Line(p[3], p[0], p[0].type, 0), 84 ] 85 86 def encode(self) -> tuple[str, bytes]: 87 """ 88 Encodes the image in its embedded format (JPEG) without 89 converting it. All other formats are encoded as lossless PNG. 90 91 :return: The file suffix and content of the image file. 92 """ 93 filters = self.get_filters(skip_simple=True) 94 if filters == ["DCTDecode"]: 95 return ".jpg", bytes(self.get_data(decode_simple=True)) 96 97 bitmap = self.get_bitmap() 98 channels, width, height = bitmap.n_channels, bitmap.width, bitmap.height 99 # PNG color types: grayscale, RGB, RGBA 100 color_type = {1: 0, 3: 2, 4: 6}[channels] 101 buffer = bytes(bitmap.buffer) 102 rows = bytearray() 103 for yy in range(height): 104 row = bytearray(buffer[yy * bitmap.stride : yy * bitmap.stride + width * channels]) 105 if channels >= 3: 106 # The bitmap is BGR(A) or BGRx 107 row[0::channels], row[2::channels] = row[2::channels], row[0::channels] 108 if bitmap.format == pp.raw.FPDFBitmap_BGRx: 109 row[3::4] = b"\xff" * width 110 rows += b"\0" + row 111 112 def _chunk(name: bytes, data: bytes) -> bytes: 113 return struct.pack(">I", len(data)) + name + data + struct.pack(">I", zlib.crc32(name + data)) 114 115 return ".png", ( 116 b"\x89PNG\r\n\x1a\n" 117 + _chunk(b"IHDR", struct.pack(">IIBBBBB", width, height, 8, color_type, 0, 0, 0)) 118 + _chunk(b"IDAT", zlib.compress(bytes(rows), 9)) 119 + _chunk(b"IEND", b"") 120 ) 121 122 def __repr__(self) -> str: 123 return f"I{self.bbox}"
This class extends pypdfium2.PdfImage to align it with the interface of
the Path class so that it can be used in the same
algorithms without filtering.
You must construct the images by calling modm_data.pdf.page.Page.images.
Images are currently ignored.
27 def __init__(self, obj): 28 """ 29 :param obj: Page object of the image. 30 """ 31 super().__init__(obj.raw, obj.page, obj.pdf, obj.container, obj.level) 32 assert pp.raw.FPDFPageObj_GetType(obj.raw) == pp.raw.FPDF_PAGEOBJ_IMAGE 33 self.type = pp.raw.FPDF_PAGEOBJ_IMAGE 34 35 self.count: int = 4 36 """Number of segments. Always 4 due to rectangular image form. 37 (For compatibility with `Path.count`.)""" 38 self.stroke: int = 0 39 """The border stroke color. Always 0. 40 (For compatibility with `Path.stroke`.)""" 41 self.fill: int = 0 42 """The image fill color. Always 0. 43 (For compatibility with `Path.fill`.)""" 44 self.width: float = 0 45 """The border line width. Always 0. 46 (For compatibility with `Path.width`.)"""
Parameters
- obj: Page object of the image.
Number of segments. Always 4 due to rectangular image form.
(For compatibility with Path.count.)
48 @cached_property 49 def matrix(self) -> pp.PdfMatrix: 50 """The transformation matrix.""" 51 return self.get_matrix()
The transformation matrix.
53 @cached_property 54 def bbox(self) -> Rectangle: 55 """The bounding box of the image.""" 56 bbox = Rectangle(*self.get_bounds()) 57 if self.page.rotation: 58 bbox = Rectangle(bbox.p0.y, self.page.height - bbox.p1.x, bbox.p1.y, self.page.height - bbox.p0.x) 59 return bbox
The bounding box of the image.
61 @cached_property 62 def points(self) -> list[Point]: 63 """ 64 The 4 points of the bounding box. 65 (For compatibility with `Path.points`.) 66 """ 67 points = self.bbox.points 68 if self.page.rotation: 69 points = [Point(p.y, self.page.height - p.x, p.type) for p in points] 70 return points
The 4 points of the bounding box.
(For compatibility with Path.points.)
72 @cached_property 73 def lines(self) -> list[Line]: 74 """ 75 The 4 lines of the bounding box. 76 (For compatibility with `Path.lines`.) 77 """ 78 p = self.points 79 return [ 80 Line(p[0], p[1], p[1].type, 0), 81 Line(p[1], p[2], p[2].type, 0), 82 Line(p[2], p[3], p[3].type, 0), 83 Line(p[3], p[0], p[0].type, 0), 84 ]
The 4 lines of the bounding box.
(For compatibility with Path.lines.)
86 def encode(self) -> tuple[str, bytes]: 87 """ 88 Encodes the image in its embedded format (JPEG) without 89 converting it. All other formats are encoded as lossless PNG. 90 91 :return: The file suffix and content of the image file. 92 """ 93 filters = self.get_filters(skip_simple=True) 94 if filters == ["DCTDecode"]: 95 return ".jpg", bytes(self.get_data(decode_simple=True)) 96 97 bitmap = self.get_bitmap() 98 channels, width, height = bitmap.n_channels, bitmap.width, bitmap.height 99 # PNG color types: grayscale, RGB, RGBA 100 color_type = {1: 0, 3: 2, 4: 6}[channels] 101 buffer = bytes(bitmap.buffer) 102 rows = bytearray() 103 for yy in range(height): 104 row = bytearray(buffer[yy * bitmap.stride : yy * bitmap.stride + width * channels]) 105 if channels >= 3: 106 # The bitmap is BGR(A) or BGRx 107 row[0::channels], row[2::channels] = row[2::channels], row[0::channels] 108 if bitmap.format == pp.raw.FPDFBitmap_BGRx: 109 row[3::4] = b"\xff" * width 110 rows += b"\0" + row 111 112 def _chunk(name: bytes, data: bytes) -> bytes: 113 return struct.pack(">I", len(data)) + name + data + struct.pack(">I", zlib.crc32(name + data)) 114 115 return ".png", ( 116 b"\x89PNG\r\n\x1a\n" 117 + _chunk(b"IHDR", struct.pack(">IIBBBBB", width, height, 8, color_type, 0, 0, 0)) 118 + _chunk(b"IDAT", zlib.compress(bytes(rows), 9)) 119 + _chunk(b"IEND", b"") 120 )
Encodes the image in its embedded format (JPEG) without converting it. All other formats are encoded as lossless PNG.
Returns
The file suffix and content of the image file.
Inherited Members
- pypdfium2._helpers.pageobjects.PdfImage
- SIMPLE_FILTERS
- new
- get_metadata
- get_px_size
- load_jpeg
- set_bitmap
- get_bitmap
- get_data
- get_filters
- extract
- pypdfium2._helpers.pageobjects.PdfObject
- raw
- page
- container
- level
- parent
- get_bounds
- get_quad_points
- get_matrix
- set_matrix
- transform
- pypdfium2.internal.bases.AutoCloseable
- close
11class ObjLink: 12 """ 13 An internal reference to other objects by an identifier giving the bounding 14 box and destination page. These links can be extracted by calling the 15 `modm_data.pdf.page.Page.objlinks` property. 16 """ 17 18 def __init__(self, page: "modm_data.pdf.Page", link: pp.raw.FPDF_LINK): # noqa: F821 19 """ 20 :param page: Page containing the link, used to compute bounding box. 21 :param link: Raw link object. 22 """ 23 self._page = page 24 self._dest = pp.raw.FPDFLink_GetDest(page.pdf, link) 25 26 bbox = pp.raw.FS_RECTF() 27 assert pp.raw.FPDFLink_GetAnnotRect(link, bbox) 28 bbox = Rectangle(bbox) 29 if page.rotation: 30 bbox = Rectangle(bbox.p0.y, page.height - bbox.p1.x, bbox.p1.y, page.height - bbox.p0.x) 31 self.bbox: Rectangle = bbox 32 """Bounding box of the link source""" 33 34 @cached_property 35 def page_index(self) -> int: 36 """0-indexed page number of the link destination.""" 37 return pp.raw.FPDFDest_GetDestPageIndex(self._page.pdf, self._dest) 38 39 def __repr__(self) -> str: 40 return f"Obj({self.page_index})"
An internal reference to other objects by an identifier giving the bounding
box and destination page. These links can be extracted by calling the
modm_data.pdf.page.Page.objlinks property.
18 def __init__(self, page: "modm_data.pdf.Page", link: pp.raw.FPDF_LINK): # noqa: F821 19 """ 20 :param page: Page containing the link, used to compute bounding box. 21 :param link: Raw link object. 22 """ 23 self._page = page 24 self._dest = pp.raw.FPDFLink_GetDest(page.pdf, link) 25 26 bbox = pp.raw.FS_RECTF() 27 assert pp.raw.FPDFLink_GetAnnotRect(link, bbox) 28 bbox = Rectangle(bbox) 29 if page.rotation: 30 bbox = Rectangle(bbox.p0.y, page.height - bbox.p1.x, bbox.p1.y, page.height - bbox.p0.x) 31 self.bbox: Rectangle = bbox 32 """Bounding box of the link source"""
Parameters
- page: Page containing the link, used to compute bounding box.
- link: Raw link object.
43class WebLink: 44 """ 45 An external reference to URLs giving the bounding box and destination URL. 46 These links can be extracted by calling the 47 `modm_data.pdf.page.Page.weblinks` property. 48 """ 49 50 def __init__(self, page: "modm_data.pdf.Page", index: int): # noqa: F821 51 """ 52 :param page: Page containing the link, used to compute bounding box. 53 :param index: 0-index of the weblink object. 54 """ 55 self._page = page 56 self._link = page._linkpage 57 self._index = index 58 59 @cached_property 60 def bbox_count(self) -> int: 61 """The number of bounding boxes associated with this weblink.""" 62 return pp.raw.FPDFLink_CountRects(self._link, self._index) 63 64 @cached_property 65 def bboxes(self) -> list[Rectangle]: 66 """The bounding boxes associated with this weblink.""" 67 bboxes = [] 68 for ii in range(self.bbox_count): 69 x0, y0 = ctypes.c_double(), ctypes.c_double() 70 x1, y1 = ctypes.c_double(), ctypes.c_double() 71 assert pp.raw.FPDFLink_GetRect(self._link, self._index, ii, x0, y1, x1, y0) 72 bboxes.append(Rectangle(x0.value, y0.value, x1.value, y1.value)) 73 if self._page.rotation: 74 bboxes = [ 75 Rectangle(bbox.p0.y, self._page.height - bbox.p1.x, bbox.p1.y, self._page.height - bbox.p0.x) 76 for bbox in bboxes 77 ] 78 return bboxes 79 80 @cached_property 81 def range(self) -> tuple[int, int]: 82 """Start and end index of the characters associated with this link.""" 83 cstart = ctypes.c_int() 84 ccount = ctypes.c_int() 85 assert pp.raw.FPDFLink_GetTextRange(self._link, self._index, cstart, ccount) 86 return (cstart.value, cstart.value + ccount.value) 87 88 @cached_property 89 def url(self) -> str: 90 """The URL string of this link.""" 91 length = 1000 92 cbuffer = ctypes.c_ushort * length 93 cbuffer = cbuffer() 94 retlen = pp.raw.FPDFLink_GetURL(self._link, self._index, cbuffer, length) 95 assert retlen < length 96 return bytes(cbuffer).decode("utf-16-le").strip("\x00") 97 98 def __repr__(self) -> str: 99 return f"Url({self.url})"
An external reference to URLs giving the bounding box and destination URL.
These links can be extracted by calling the
modm_data.pdf.page.Page.weblinks property.
50 def __init__(self, page: "modm_data.pdf.Page", index: int): # noqa: F821 51 """ 52 :param page: Page containing the link, used to compute bounding box. 53 :param index: 0-index of the weblink object. 54 """ 55 self._page = page 56 self._link = page._linkpage 57 self._index = index
Parameters
- page: Page containing the link, used to compute bounding box.
- index: 0-index of the weblink object.
59 @cached_property 60 def bbox_count(self) -> int: 61 """The number of bounding boxes associated with this weblink.""" 62 return pp.raw.FPDFLink_CountRects(self._link, self._index)
The number of bounding boxes associated with this weblink.
64 @cached_property 65 def bboxes(self) -> list[Rectangle]: 66 """The bounding boxes associated with this weblink.""" 67 bboxes = [] 68 for ii in range(self.bbox_count): 69 x0, y0 = ctypes.c_double(), ctypes.c_double() 70 x1, y1 = ctypes.c_double(), ctypes.c_double() 71 assert pp.raw.FPDFLink_GetRect(self._link, self._index, ii, x0, y1, x1, y0) 72 bboxes.append(Rectangle(x0.value, y0.value, x1.value, y1.value)) 73 if self._page.rotation: 74 bboxes = [ 75 Rectangle(bbox.p0.y, self._page.height - bbox.p1.x, bbox.p1.y, self._page.height - bbox.p0.x) 76 for bbox in bboxes 77 ] 78 return bboxes
The bounding boxes associated with this weblink.
80 @cached_property 81 def range(self) -> tuple[int, int]: 82 """Start and end index of the characters associated with this link.""" 83 cstart = ctypes.c_int() 84 ccount = ctypes.c_int() 85 assert pp.raw.FPDFLink_GetTextRange(self._link, self._index, cstart, ccount) 86 return (cstart.value, cstart.value + ccount.value)
Start and end index of the characters associated with this link.
88 @cached_property 89 def url(self) -> str: 90 """The URL string of this link.""" 91 length = 1000 92 cbuffer = ctypes.c_ushort * length 93 cbuffer = cbuffer() 94 retlen = pp.raw.FPDFLink_GetURL(self._link, self._index, cbuffer, length) 95 assert retlen < length 96 return bytes(cbuffer).decode("utf-16-le").strip("\x00")
The URL string of this link.
11class Structure: 12 """ 13 A tagged PDF/UA (Universal Accessibility) contains the structure of content 14 as a tree data structure with similar semantics to HTML. Sadly, the quality 15 of the tags depends heavily on the PDF creation software. See 16 [Overview of PDF tags](https://accessible-pdf.info/en/basics/general/overview-of-the-pdf-tags/). 17 18 An example of an accessible pdf that can be inspected via these classes: 19 [Rock On, D.C. Music Festival](https://commonlook.com/wp-content/uploads/2020/04/accessible-pdf-example.pdf). 20 21 This class is a convenience wrapper around [the pdfium structtree methods]( 22 https://pdfium.googlesource.com/pdfium/+/main/public/fpdf_structtree.h). 23 """ 24 25 def __init__(self, page: "modm_data.pdf.page.Page", element: pp.raw.FPDF_STRUCTELEMENT, parent: "Structure" = None): # noqa: F821 26 self._page = page 27 self._element = element 28 self.parent: Structure = weakref.ref(parent) if parent else None 29 """The parent node.""" 30 31 def _get_string(self, function) -> str: 32 length = function(self._element, 0, 0) 33 clength = ctypes.c_ulong(length) 34 cbuffer = ctypes.create_string_buffer(length) 35 function(self._element, cbuffer, clength) 36 return bytes(cbuffer).decode("utf-16-le", errors="ignore") 37 38 @cached_property 39 def title(self) -> str: 40 """Title `/T`""" 41 return self._get_string(pp.raw.FPDF_StructElement_GetTitle) 42 43 @cached_property 44 def actual_text(self) -> str: 45 """The actual text.""" 46 return self._get_string(pp.raw.FPDF_StructElement_GetActualText) 47 48 @cached_property 49 def alt_text(self) -> str: 50 """Alternate Text""" 51 return self._get_string(pp.raw.FPDF_StructElement_GetAltText) 52 53 @cached_property 54 def type(self) -> str: 55 """Type `/S`""" 56 return self._get_string(pp.raw.FPDF_StructElement_GetType) 57 58 @cached_property 59 def obj_type(self) -> str: 60 """Object Type `/Type`""" 61 return self._get_string(pp.raw.FPDF_StructElement_GetObjType) 62 63 @cached_property 64 def language(self) -> str: 65 """The case-insensitive IETF BCP 47 language code.""" 66 return self._get_string(pp.raw.FPDF_StructElement_GetLang) 67 68 @cached_property 69 def id(self) -> str: 70 """Identifier""" 71 return self._get_string(pp.raw.FPDF_StructElement_GetID) 72 73 @cached_property 74 def marked_ids(self) -> list[int]: 75 """List of marked content identifiers""" 76 ids = [] 77 for index in range(pp.raw.FPDF_StructElement_GetMarkedContentIdCount(self._element)): 78 if (mcid := pp.raw.FPDF_StructElement_GetMarkedContentIdAtIndex(self._element, index)) != -1: 79 ids.append(mcid) 80 return ids 81 82 @cached_property 83 def attributes(self) -> dict[str, str | bool | float]: 84 """ 85 All attributes of this structure element as a dictionary. 86 87 .. note:: 88 Due to limitations of the pdfium API, attribute arrays cannot be 89 extracted! The values are marked as `[?]` in the dictionary. 90 """ 91 kv = {} 92 for eindex in range(pp.raw.FPDF_StructElement_GetAttributeCount(self._element)): 93 attr = pp.raw.FPDF_StructElement_GetAttributeAtIndex(self._element, eindex) 94 for aindex in range(pp.raw.FPDF_StructElement_Attr_GetCount(attr)): 95 # Get the name 96 clength = ctypes.c_ulong(0) 97 cname = ctypes.create_string_buffer(1) # workaround to get length 98 assert pp.raw.FPDF_StructElement_Attr_GetName(attr, aindex, cname, 0, clength) 99 cname = ctypes.create_string_buffer(clength.value) 100 assert pp.raw.FPDF_StructElement_Attr_GetName(attr, aindex, cname, clength, clength) 101 name = cname.raw.decode("utf-8", errors="ignore") 102 103 # Get the type 104 atype = pp.raw.FPDF_StructElement_Attr_GetType(attr, cname) 105 assert atype != pp.raw.FPDF_OBJECT_UNKNOWN 106 107 # Then get each type individually 108 match atype: 109 case pp.raw.FPDF_OBJECT_BOOLEAN: 110 cbool = ctypes.bool() 111 assert pp.raw.FPDF_StructElement_Attr_GetBooleanValue(attr, cname, cbool) 112 kv[name] = cbool.value 113 114 case pp.raw.FPDF_OBJECT_NUMBER: 115 cfloat = ctypes.c_float() 116 assert pp.raw.FPDF_StructElement_Attr_GetNumberValue(attr, cname, cfloat) 117 kv[name] = cfloat.value 118 119 case pp.raw.FPDF_OBJECT_STRING | pp.raw.FPDF_OBJECT_NAME: 120 assert pp.raw.FPDF_StructElement_Attr_GetStringValue(attr, cname, 0, 0, clength) 121 cattrname = ctypes.create_string_buffer(clength.value * 2) 122 assert pp.raw.FPDF_StructElement_Attr_GetStringValue(attr, cname, cattrname, clength, clength) 123 kv[name] = cattrname.raw.decode("utf-16-le", errors="ignore")[: clength.value - 1] 124 125 # FIXME: FPDF_OBJECT_ARRAY is not a blob, but no other APIs are exposed? 126 # case pp.raw.FPDF_OBJECT_ARRAY: 127 # assert pp.raw.FPDF_StructElement_Attr_GetBlobValue(attr, cname, 0, 0, clength) 128 # cblob = ctypes.create_string_buffer(clength.value) 129 # assert pp.raw.FPDF_StructElement_Attr_GetBlobValue(attr, cname, cblob, clength, clength) 130 # kv[name] = cblob.raw 131 132 case pp.raw.FPDF_OBJECT_ARRAY: 133 kv[name] = "[?]" 134 135 case _: 136 kv[name] = f"[unknown={atype}?]" 137 return kv 138 139 @cache 140 def child(self, index: int) -> "Structure": 141 """ 142 :param index: 0-index of child. 143 :return: Child structure. 144 """ 145 index = pp.raw.FPDF_StructElement_GetChildAtIndex(self._element, index) 146 return Structure(self._page, index, self) 147 148 @property 149 def children(self) -> list: 150 """All child structures.""" 151 count = pp.raw.FPDF_StructElement_CountChildren(self._element) 152 for ii in range(count): 153 yield self.child(ii) 154 155 def descr(self, indent=0) -> str: 156 """Description including all children via indentation.""" 157 string = " " * indent + repr(self) + "\n" 158 for child in self.children: 159 string += child.descr(indent + 4) 160 return string 161 162 def __repr__(self) -> str: 163 values = [] 164 if self.type: 165 values.append(f"type={self.type}") 166 if self.title: 167 values.append(f"title={self.title}") 168 if self.actual_text: 169 values.append(f"act_text={self.actual_text}") 170 if self.alt_text: 171 values.append(f"alt_text={self.alt_text}") 172 if self.id: 173 values.append(f"id={self.id}") 174 values += [f"mid={i}" for i in self.marked_ids] 175 values += [f"{k}={v}" for k, v in self.attributes.items()] 176 return f"S({','.join(map(str, values))})"
A tagged PDF/UA (Universal Accessibility) contains the structure of content as a tree data structure with similar semantics to HTML. Sadly, the quality of the tags depends heavily on the PDF creation software. See Overview of PDF tags.
An example of an accessible pdf that can be inspected via these classes: Rock On, D.C. Music Festival.
This class is a convenience wrapper around the pdfium structtree methods.
38 @cached_property 39 def title(self) -> str: 40 """Title `/T`""" 41 return self._get_string(pp.raw.FPDF_StructElement_GetTitle)
Title /T
43 @cached_property 44 def actual_text(self) -> str: 45 """The actual text.""" 46 return self._get_string(pp.raw.FPDF_StructElement_GetActualText)
The actual text.
48 @cached_property 49 def alt_text(self) -> str: 50 """Alternate Text""" 51 return self._get_string(pp.raw.FPDF_StructElement_GetAltText)
Alternate Text
53 @cached_property 54 def type(self) -> str: 55 """Type `/S`""" 56 return self._get_string(pp.raw.FPDF_StructElement_GetType)
Type /S
58 @cached_property 59 def obj_type(self) -> str: 60 """Object Type `/Type`""" 61 return self._get_string(pp.raw.FPDF_StructElement_GetObjType)
Object Type /Type
63 @cached_property 64 def language(self) -> str: 65 """The case-insensitive IETF BCP 47 language code.""" 66 return self._get_string(pp.raw.FPDF_StructElement_GetLang)
The case-insensitive IETF BCP 47 language code.
68 @cached_property 69 def id(self) -> str: 70 """Identifier""" 71 return self._get_string(pp.raw.FPDF_StructElement_GetID)
Identifier
73 @cached_property 74 def marked_ids(self) -> list[int]: 75 """List of marked content identifiers""" 76 ids = [] 77 for index in range(pp.raw.FPDF_StructElement_GetMarkedContentIdCount(self._element)): 78 if (mcid := pp.raw.FPDF_StructElement_GetMarkedContentIdAtIndex(self._element, index)) != -1: 79 ids.append(mcid) 80 return ids
List of marked content identifiers
82 @cached_property 83 def attributes(self) -> dict[str, str | bool | float]: 84 """ 85 All attributes of this structure element as a dictionary. 86 87 .. note:: 88 Due to limitations of the pdfium API, attribute arrays cannot be 89 extracted! The values are marked as `[?]` in the dictionary. 90 """ 91 kv = {} 92 for eindex in range(pp.raw.FPDF_StructElement_GetAttributeCount(self._element)): 93 attr = pp.raw.FPDF_StructElement_GetAttributeAtIndex(self._element, eindex) 94 for aindex in range(pp.raw.FPDF_StructElement_Attr_GetCount(attr)): 95 # Get the name 96 clength = ctypes.c_ulong(0) 97 cname = ctypes.create_string_buffer(1) # workaround to get length 98 assert pp.raw.FPDF_StructElement_Attr_GetName(attr, aindex, cname, 0, clength) 99 cname = ctypes.create_string_buffer(clength.value) 100 assert pp.raw.FPDF_StructElement_Attr_GetName(attr, aindex, cname, clength, clength) 101 name = cname.raw.decode("utf-8", errors="ignore") 102 103 # Get the type 104 atype = pp.raw.FPDF_StructElement_Attr_GetType(attr, cname) 105 assert atype != pp.raw.FPDF_OBJECT_UNKNOWN 106 107 # Then get each type individually 108 match atype: 109 case pp.raw.FPDF_OBJECT_BOOLEAN: 110 cbool = ctypes.bool() 111 assert pp.raw.FPDF_StructElement_Attr_GetBooleanValue(attr, cname, cbool) 112 kv[name] = cbool.value 113 114 case pp.raw.FPDF_OBJECT_NUMBER: 115 cfloat = ctypes.c_float() 116 assert pp.raw.FPDF_StructElement_Attr_GetNumberValue(attr, cname, cfloat) 117 kv[name] = cfloat.value 118 119 case pp.raw.FPDF_OBJECT_STRING | pp.raw.FPDF_OBJECT_NAME: 120 assert pp.raw.FPDF_StructElement_Attr_GetStringValue(attr, cname, 0, 0, clength) 121 cattrname = ctypes.create_string_buffer(clength.value * 2) 122 assert pp.raw.FPDF_StructElement_Attr_GetStringValue(attr, cname, cattrname, clength, clength) 123 kv[name] = cattrname.raw.decode("utf-16-le", errors="ignore")[: clength.value - 1] 124 125 # FIXME: FPDF_OBJECT_ARRAY is not a blob, but no other APIs are exposed? 126 # case pp.raw.FPDF_OBJECT_ARRAY: 127 # assert pp.raw.FPDF_StructElement_Attr_GetBlobValue(attr, cname, 0, 0, clength) 128 # cblob = ctypes.create_string_buffer(clength.value) 129 # assert pp.raw.FPDF_StructElement_Attr_GetBlobValue(attr, cname, cblob, clength, clength) 130 # kv[name] = cblob.raw 131 132 case pp.raw.FPDF_OBJECT_ARRAY: 133 kv[name] = "[?]" 134 135 case _: 136 kv[name] = f"[unknown={atype}?]" 137 return kv
All attributes of this structure element as a dictionary.
Due to limitations of the pdfium API, attribute arrays cannot be
extracted! The values are marked as [?] in the dictionary.
139 @cache 140 def child(self, index: int) -> "Structure": 141 """ 142 :param index: 0-index of child. 143 :return: Child structure. 144 """ 145 index = pp.raw.FPDF_StructElement_GetChildAtIndex(self._element, index) 146 return Structure(self._page, index, self)
Parameters
- index: 0-index of child.
Returns
Child structure.
148 @property 149 def children(self) -> list: 150 """All child structures.""" 151 count = pp.raw.FPDF_StructElement_CountChildren(self._element) 152 for ii in range(count): 153 yield self.child(ii)
All child structures.
155 def descr(self, indent=0) -> str: 156 """Description including all children via indentation.""" 157 string = " " * indent + repr(self) + "\n" 158 for child in self.children: 159 string += child.descr(indent + 4) 160 return string
Description including all children via indentation.