-
Notifications
You must be signed in to change notification settings - Fork 1.6k
ENH: Check whether image is displayed on a given page #3738
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 26 commits
57390d1
ff16713
cf232f9
4d1ca4e
672ad47
069d5c5
ff80e2e
dd2ac2e
27fc2bb
5f59487
a09a6bb
58c75a6
2966ee2
3bcf9a5
14a56d7
e7f78cf
b2a6114
f0de97d
6cad13a
69cb462
54d6dd2
6db1389
f0c7a72
18ebf94
983022f
d6b7ff4
973f345
6f0aa8b
183e10f
ccf4a9d
42c1f81
364ccbf
683d5d4
70963f6
439fab3
e4ea241
38eebdb
bb11c8c
743d023
00411b1
677e088
92a93ac
5918e8b
38f5544
4302443
749ee97
5d40adb
2a8fdb1
1015999
956a4bf
f2f8617
3f98544
44ab6ae
cf03742
f3f828a
b1290a2
b6aa4c0
9501d67
0848260
d278768
216c9db
5159e13
6bf79f8
2994cbf
ecf3716
207f1f4
85579d6
1540d3b
9ecac76
bc4c931
a5b2054
fbcd1e7
7b2bf6c
ec977c3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | |||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -55,6 +55,7 @@ | ||||||||||||||||||||||||||||
| TransformationMatrixType, | |||||||||||||||||||||||||||||
| _human_readable_bytes, | |||||||||||||||||||||||||||||
| deprecate, | |||||||||||||||||||||||||||||
| deprecate_with_replacement, | |||||||||||||||||||||||||||||
| logger_warning, | |||||||||||||||||||||||||||||
| matrix_multiply, | |||||||||||||||||||||||||||||
| ) | |||||||||||||||||||||||||||||
|
|
@@ -359,6 +360,18 @@ class ImageFile: | ||||||||||||||||||||||||||||
| Reference to the object storing the stream. | |||||||||||||||||||||||||||||
| """ | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| is_inline: bool = False | |||||||||||||||||||||||||||||
| """ | |||||||||||||||||||||||||||||
| True if this is an inline image (~0~, ~1~, etc.). | |||||||||||||||||||||||||||||
| """ | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| is_displayed: bool = False | |||||||||||||||||||||||||||||
| """ | |||||||||||||||||||||||||||||
| True if this image is displayed in the page content stream. | |||||||||||||||||||||||||||||
| Some PDFs duplicate image references over all the pages, | |||||||||||||||||||||||||||||
| so this is needed to disambiguate. | |||||||||||||||||||||||||||||
| """ | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| def replace(self, new_image: Image, **kwargs: Any) -> None: | |||||||||||||||||||||||||||||
| """ | |||||||||||||||||||||||||||||
| Replace the image with a new PIL image. | |||||||||||||||||||||||||||||
|
|
@@ -512,7 +525,7 @@ def __init__( | ||||||||||||||||||||||||||||
| ) -> None: | |||||||||||||||||||||||||||||
| DictionaryObject.__init__(self) | |||||||||||||||||||||||||||||
| self.pdf = pdf | |||||||||||||||||||||||||||||
| self.inline_images: Optional[dict[str, ImageFile]] = None | |||||||||||||||||||||||||||||
| self._displayed_images: Optional[dict[str, ImageFile]] = None | |||||||||||||||||||||||||||||
| self.indirect_reference = indirect_reference | |||||||||||||||||||||||||||||
| if not is_null_or_none(indirect_reference): | |||||||||||||||||||||||||||||
| assert indirect_reference is not None, "mypy" | |||||||||||||||||||||||||||||
|
|
@@ -608,8 +621,8 @@ def _get_ids_image( | ||||||||||||||||||||||||||||
| if _i in call_stack: | |||||||||||||||||||||||||||||
| return [] | |||||||||||||||||||||||||||||
| call_stack.append(_i) | |||||||||||||||||||||||||||||
| if self.inline_images is None: | |||||||||||||||||||||||||||||
| self.inline_images = self._get_inline_images() | |||||||||||||||||||||||||||||
| if self._displayed_images is None: | |||||||||||||||||||||||||||||
| self._displayed_images = self._parse_images_from_content_stream() | |||||||||||||||||||||||||||||
| if obj is None: | |||||||||||||||||||||||||||||
| obj = self | |||||||||||||||||||||||||||||
| if ancest is None: | |||||||||||||||||||||||||||||
|
|
@@ -620,19 +633,42 @@ def _get_ids_image( | ||||||||||||||||||||||||||||
| is_null_or_none(resources := obj[PG.RESOURCES]) or | |||||||||||||||||||||||||||||
| RES.XOBJECT not in cast(DictionaryObject, resources) | |||||||||||||||||||||||||||||
| ): | |||||||||||||||||||||||||||||
| return [] if self.inline_images is None else list(self.inline_images.keys()) | |||||||||||||||||||||||||||||
| return [] if self._displayed_images is None else list(self._displayed_images.keys()) | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| x_object = resources[RES.XOBJECT].get_object() # type: ignore | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| # Iterate through all XObject resources | |||||||||||||||||||||||||||||
| for o in x_object: | |||||||||||||||||||||||||||||
| # Skip non-stream objects (only process StreamObject) | |||||||||||||||||||||||||||||
| if not isinstance(x_object[o], StreamObject): | |||||||||||||||||||||||||||||
| continue | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| # Check if this XObject is an Image | |||||||||||||||||||||||||||||
| if x_object[o][ImageAttributes.SUBTYPE] == "/Image": | |||||||||||||||||||||||||||||
| # Add the image ID (with ancestry if needed) | |||||||||||||||||||||||||||||
| # When ancest is empty, o is top-level: "/I0" | |||||||||||||||||||||||||||||
| # When ancest is not empty, [ancest, o] is nested: ["/Form1", "/I0"] | |||||||||||||||||||||||||||||
| lst.append(o if len(ancest) == 0 else [*ancest, o]) | |||||||||||||||||||||||||||||
| else: # is a form with possible images inside | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| # If it's a form, recursively search for images inside it | |||||||||||||||||||||||||||||
| else: | |||||||||||||||||||||||||||||
| # Forms may contain images that are Do-referenced in their content stream | |||||||||||||||||||||||||||||
| lst.extend(self._get_ids_image(x_object[o], [*ancest, o], call_stack)) | |||||||||||||||||||||||||||||
| assert self.inline_images is not None | |||||||||||||||||||||||||||||
|
stefan6419846 marked this conversation as resolved.
|
|||||||||||||||||||||||||||||
| lst.extend(list(self.inline_images.keys())) | |||||||||||||||||||||||||||||
| return lst | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| # Removes duplicates and preserves order | |||||||||||||||||||||||||||||
|
andreasntr marked this conversation as resolved.
|
|||||||||||||||||||||||||||||
| deduplicated = [] | |||||||||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Where are we getting duplicates from?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. xojb names matching with do images referencing them |
|||||||||||||||||||||||||||||
| for item in lst: | |||||||||||||||||||||||||||||
| if item not in deduplicated: | |||||||||||||||||||||||||||||
| deduplicated.append(item) | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| # Add inline images (they may overlap with XObject images) | |||||||||||||||||||||||||||||
| # Preserves order | |||||||||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How does this preserve order? The attribute is a regular dictionary, where we should not assume a fixed order. Additionally, can we really expect overlaps? How would they look like? If we want to remove duplicates, where aren't we collecting the data as a set where we can eliminate explicit duplicate checks?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. duplicates happen when xobjs are referenced by do images since they share the same name. Order preservation is intended as "keep the order in which they are resolved from the pdf content"
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are we really giving any guarantees about the order? Or is this just required for testing purposes? A set would avoid all of this hassle.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i remembered why i excluded sets in the first place: |
|||||||||||||||||||||||||||||
| # Inline images have names starting with ~ (e.g., ~0~, ~1~) | |||||||||||||||||||||||||||||
| for k in self._displayed_images: | |||||||||||||||||||||||||||||
| if k not in deduplicated: | |||||||||||||||||||||||||||||
| deduplicated.append(k) | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| return deduplicated | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| def _get_image( | |||||||||||||||||||||||||||||
| self, | |||||||||||||||||||||||||||||
|
|
@@ -657,13 +693,22 @@ def _get_image( | ||||||||||||||||||||||||||||
| ) from exc | |||||||||||||||||||||||||||||
| if isinstance(id, str): | |||||||||||||||||||||||||||||
| if id[0] == "~" and id[-1] == "~": | |||||||||||||||||||||||||||||
| if self.inline_images is None: | |||||||||||||||||||||||||||||
| self.inline_images = self._get_inline_images() | |||||||||||||||||||||||||||||
| if self.inline_images is None: | |||||||||||||||||||||||||||||
| if self._displayed_images is None: | |||||||||||||||||||||||||||||
| self._displayed_images = self._parse_images_from_content_stream() | |||||||||||||||||||||||||||||
| if self._displayed_images is None: | |||||||||||||||||||||||||||||
| raise KeyError("No inline image can be found") | |||||||||||||||||||||||||||||
| return self.inline_images[id] | |||||||||||||||||||||||||||||
| img = self._displayed_images[id] | |||||||||||||||||||||||||||||
| img.is_inline = True | |||||||||||||||||||||||||||||
| img.is_displayed = True | |||||||||||||||||||||||||||||
| return img | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| assert xobjs is not None | |||||||||||||||||||||||||||||
| # Check if image is in content stream (from _parse_images_from_content_stream) | |||||||||||||||||||||||||||||
| if self._displayed_images and id in self._displayed_images: | |||||||||||||||||||||||||||||
| img = self._displayed_images[id] | |||||||||||||||||||||||||||||
| img.is_inline = False | |||||||||||||||||||||||||||||
| return img | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| from .generic._image_xobject import _xobj_to_image # noqa: PLC0415 | |||||||||||||||||||||||||||||
| imgd = _xobj_to_image(cast(DictionaryObject, xobjs[id])) | |||||||||||||||||||||||||||||
| extension, byte_stream = imgd[:2] | |||||||||||||||||||||||||||||
|
|
@@ -672,6 +717,8 @@ def _get_image( | ||||||||||||||||||||||||||||
| data=byte_stream, | |||||||||||||||||||||||||||||
| image=imgd[2], | |||||||||||||||||||||||||||||
| indirect_reference=xobjs[id].indirect_reference, | |||||||||||||||||||||||||||||
| is_inline=False, | |||||||||||||||||||||||||||||
| is_displayed=False, # XObject images from resources only (not in content stream) | |||||||||||||||||||||||||||||
| ) | |||||||||||||||||||||||||||||
| # in a subobject | |||||||||||||||||||||||||||||
| assert xobjs is not None | |||||||||||||||||||||||||||||
|
|
@@ -701,7 +748,9 @@ def images(self) -> VirtualListImages: | ||||||||||||||||||||||||||||
| * `.name` : name of the object | |||||||||||||||||||||||||||||
| * `.data` : bytes of the object | |||||||||||||||||||||||||||||
| * `.image` : PIL Image Object | |||||||||||||||||||||||||||||
| * `.indirect_reference` : object reference | |||||||||||||||||||||||||||||
| * `.indirect_reference` : object reference (None for inline images) | |||||||||||||||||||||||||||||
| * `.is_inline` : True for inline images (~0~, ~1~...), False for XObjects | |||||||||||||||||||||||||||||
| * `.is_displayed` : True for images found in content stream, False otherwise | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| and the following methods: | |||||||||||||||||||||||||||||
| `.replace(new_image: PIL.Image.Image, **kwargs)` : | |||||||||||||||||||||||||||||
|
|
@@ -712,12 +761,49 @@ def images(self) -> VirtualListImages: | ||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| reader.pages[0].images[0].replace(Image.open("new_image.jpg"), quality=20) | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| Inline images are extracted and named ~0~, ~1~, ..., with the | |||||||||||||||||||||||||||||
| indirect_reference set to None. | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| """ | |||||||||||||||||||||||||||||
| return VirtualListImages(self._get_ids_image, self._get_image) | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| @property | |||||||||||||||||||||||||||||
| def inline_images(self) -> Optional[dict[str, ImageFile]]: | |||||||||||||||||||||||||||||
| """ | |||||||||||||||||||||||||||||
| Return only inline images from the page. | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| .. deprecated:: | |||||||||||||||||||||||||||||
|
andreasntr marked this conversation as resolved.
|
|||||||||||||||||||||||||||||
| Use :attr:`images` and filter by :attr:`ImageFile.is_inline` instead. | |||||||||||||||||||||||||||||
| This property will be removed in pypdf 7.0. | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| Examples: | |||||||||||||||||||||||||||||
| >>> from pypdf import PdfReader | |||||||||||||||||||||||||||||
| >>> reader = PdfReader("example.pdf") | |||||||||||||||||||||||||||||
| >>> page = reader.pages[0] | |||||||||||||||||||||||||||||
| >>> inline_images = {k: v for k, v in page.images.items() if v.is_inline} | |||||||||||||||||||||||||||||
| """ | |||||||||||||||||||||||||||||
| deprecate_with_replacement( | |||||||||||||||||||||||||||||
| "PageObject.inline_images", | |||||||||||||||||||||||||||||
| "PageObject.images", | |||||||||||||||||||||||||||||
| "7.0", | |||||||||||||||||||||||||||||
| ) | |||||||||||||||||||||||||||||
| if self._displayed_images is None: | |||||||||||||||||||||||||||||
| return None | |||||||||||||||||||||||||||||
| return {k: v for k, v in self._displayed_images.items() if v.is_inline} | |||||||||||||||||||||||||||||
|
andreasntr marked this conversation as resolved.
Outdated
|
|||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| @inline_images.setter | |||||||||||||||||||||||||||||
| def inline_images(self, value: Optional[dict[str, ImageFile]]) -> None: | |||||||||||||||||||||||||||||
|
andreasntr marked this conversation as resolved.
|
|||||||||||||||||||||||||||||
| """ | |||||||||||||||||||||||||||||
|
andreasntr marked this conversation as resolved.
Outdated
|
|||||||||||||||||||||||||||||
| Setter for inline_images. | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| Setting to None clears the cache and forces recalculation on next access, | |||||||||||||||||||||||||||||
| emulating the previous caching control mechanism. Setting to a dict merges | |||||||||||||||||||||||||||||
| the values into the existing cache. | |||||||||||||||||||||||||||||
| """ | |||||||||||||||||||||||||||||
| if value is None: | |||||||||||||||||||||||||||||
| self._displayed_images = None | |||||||||||||||||||||||||||||
| else: | |||||||||||||||||||||||||||||
| if self._displayed_images is None: | |||||||||||||||||||||||||||||
| self._displayed_images = {} | |||||||||||||||||||||||||||||
| self._displayed_images.update(value) | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| def _translate_value_inline_image(self, k: str, v: PdfObject) -> PdfObject: | |||||||||||||||||||||||||||||
| """Translate values used in inline image""" | |||||||||||||||||||||||||||||
| try: | |||||||||||||||||||||||||||||
|
|
@@ -733,24 +819,85 @@ def _translate_value_inline_image(self, k: str, v: PdfObject) -> PdfObject: | ||||||||||||||||||||||||||||
| raise PdfReadError(f"Cannot find resource entry {v} for {k}") | |||||||||||||||||||||||||||||
| return v | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| def _get_inline_images(self) -> dict[str, ImageFile]: | |||||||||||||||||||||||||||||
| """Load inline images. Entries will be identified as `~1~`.""" | |||||||||||||||||||||||||||||
| def _parse_images_from_content_stream(self) -> dict[str, ImageFile]: | |||||||||||||||||||||||||||||
| """Load images from content stream. Includes both inline images and Do-referenced images. | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| This method scans the page content stream and extracts: | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| 1. **Inline images** (~0~, ~1~...): Embedded directly in content stream via BI/EI operators | |||||||||||||||||||||||||||||
| - is_inline=True, is_displayed=True, indirect_reference=None | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| 2. **Do-referenced images** (/Im0, /Im1...): Referenced via "Do" operator | |||||||||||||||||||||||||||||
| - is_inline=False, is_displayed=True, indirect_reference=<image object> | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| 3. **Pure XObject images** (/I0, /Image1...): Defined in Resources only (not in content stream) | |||||||||||||||||||||||||||||
| - is_inline=False, is_displayed=False, indirect_reference=<image object> | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| Returns: | |||||||||||||||||||||||||||||
| Dictionary mapping image names to ImageFile instances. | |||||||||||||||||||||||||||||
| """ | |||||||||||||||||||||||||||||
| # Idempotent: if already parsed, return cached result | |||||||||||||||||||||||||||||
| if self._displayed_images is not None: | |||||||||||||||||||||||||||||
| return self._displayed_images | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| content = self.get_contents() | |||||||||||||||||||||||||||||
| if is_null_or_none(content): | |||||||||||||||||||||||||||||
| return {} | |||||||||||||||||||||||||||||
| self._displayed_images = {} | |||||||||||||||||||||||||||||
|
andreasntr marked this conversation as resolved.
Outdated
|
|||||||||||||||||||||||||||||
| return self._displayed_images | |||||||||||||||||||||||||||||
| imgs_data = [] | |||||||||||||||||||||||||||||
| do_image_names: list[bytes] = [] | |||||||||||||||||||||||||||||
| assert content is not None, "mypy" | |||||||||||||||||||||||||||||
| for param, ope in content.operations: | |||||||||||||||||||||||||||||
| if ope == b"INLINE IMAGE": | |||||||||||||||||||||||||||||
| imgs_data.append( | |||||||||||||||||||||||||||||
| {"settings": param["settings"], "__streamdata__": param["data"]} | |||||||||||||||||||||||||||||
| ) | |||||||||||||||||||||||||||||
| elif ope == b"Do" and param: | |||||||||||||||||||||||||||||
| do_image_names.append(param[0]) # First operand is the XObject name | |||||||||||||||||||||||||||||
| elif ope in (b"BI", b"EI", b"ID"): # pragma: no cover | |||||||||||||||||||||||||||||
| raise PdfReadError( | |||||||||||||||||||||||||||||
| f"{ope!r} operator met whereas not expected, " | |||||||||||||||||||||||||||||
| "please share use case with pypdf dev team" | |||||||||||||||||||||||||||||
| ) | |||||||||||||||||||||||||||||
| # Process Do-referenced images first | |||||||||||||||||||||||||||||
| files = {} | |||||||||||||||||||||||||||||
| xobjs: Optional[DictionaryObject] = None | |||||||||||||||||||||||||||||
|
andreasntr marked this conversation as resolved.
|
|||||||||||||||||||||||||||||
| try: | |||||||||||||||||||||||||||||
| resources = cast(DictionaryObject, self[PG.RESOURCES]) | |||||||||||||||||||||||||||||
| xobjs = cast(DictionaryObject, resources[RES.XOBJECT]) | |||||||||||||||||||||||||||||
| except KeyError: | |||||||||||||||||||||||||||||
| pass # Continue with inline images only | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| if xobjs is None: | |||||||||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we need this full logic? How was this handled before?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if there is no xobject, then there cannot be any do reference to that xobject. Previously xobject images were returned even if not referenced
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am not sure if this logic should be here instead of when requesting the actual file, where we could avoid the overhead of the loop.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. here we're only extracting image names, not image contents. If we don't check whether a do reference is pointing to an image (can also be pointing to a form as far as i understand), how would we know if we need to store that name in the cache dict?
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But couldn't we store this name in the cache dict in every case? I mean, it is referenced from the page, and would be ignored on actual data retrieval?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The main point of this PR is not returning objects which are present but not displayed in the page. If we store in the cache dict the name of an image which is not actually referenced, i.e. present in the content but not actually displayed, we are back to the
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The logic of this section is to exclude non-image objects from the names, not to check whether it actually is displayed. This has been done with the If this object is referenced, it is displayed. If relevant for the
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. got it now, sorry for the confusion.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Delayed content stream processing as requested. Summary partially generated by AI (qwen 3.6 35B-A3B) Where images are discovered
How
|
|||||||||||||||||||||||||||||
| Access pattern | Behavior |
|---|---|
page.images["~0~"] (inline) |
Returns cached ImageFile |
page.images["/Im0"] (Do /Image) |
Subtype check → decode from xobjs |
page.images["/FFT0"] (Do /Form) |
KeyError: "is not an image" |
page.images["/FFT0","/Im0"] (inside form) |
Recursive call → decode from form's xobjs |
for img in page.images |
Only /Image subtype objects (non-image filtered by _get_ids_image()) |
Key design decisions
_content_stream_imagesstores ALL Do-referenced objects from the page content stream (images + forms) asNoneplaceholders._get_ids_image()filters by/Imagesubtype for image references_get_image()filters by/Imagesubtype for image references and raises if the requested object does not have/Imagesubtype.
What about pure XObjects?
They are still returned by _get_ids_image, _get_image and images but will have is_displayed=False , ideally they will be excluded by a potential displayed_images property
|
andreasntr marked this conversation as resolved.
|
Uh oh!
There was an error while loading. Please reload this page.