feat(reader): add PDF reading support with outline chapters, CLI blocks, and noise filtering (task 7)
This commit is contained in:
Vendored
+17
@@ -0,0 +1,17 @@
|
||||
"""
|
||||
PDF includes a wide variety of standard action types, whose characteristics and
|
||||
behaviour are defined by an action dictionary. These are defined in this
|
||||
submodule.
|
||||
|
||||
Trigger events are the other component of actions, and are specific to their
|
||||
associated object.
|
||||
"""
|
||||
|
||||
|
||||
from ._actions import Action, JavaScript, PageTrigger
|
||||
|
||||
__all__ = [
|
||||
"Action",
|
||||
"JavaScript",
|
||||
"PageTrigger",
|
||||
]
|
||||
Vendored
+217
@@ -0,0 +1,217 @@
|
||||
"""Action types"""
|
||||
import sys
|
||||
import uuid
|
||||
from abc import ABC
|
||||
from enum import Enum, unique
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
cast,
|
||||
)
|
||||
|
||||
from .._utils import logger_warning
|
||||
from ..constants import CatalogAttributes
|
||||
from ..errors import ParseError
|
||||
from ..generic import (
|
||||
ArrayObject,
|
||||
DictionaryObject,
|
||||
NameObject,
|
||||
NullObject,
|
||||
TextStringObject,
|
||||
create_string_object,
|
||||
is_null_or_none,
|
||||
)
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
from enum import StrEnum
|
||||
else:
|
||||
class StrEnum(str, Enum):
|
||||
def __str__(self) -> str:
|
||||
return str(self.value)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .._page import PageObject
|
||||
from .._writer import PdfWriter
|
||||
|
||||
|
||||
@unique
|
||||
class PageTrigger(StrEnum):
|
||||
"""Trigger event entries in a page object's additional-actions dictionary."""
|
||||
OPEN = "open"
|
||||
"""Trigger an action when the page is opened."""
|
||||
CLOSE = "close"
|
||||
"""Trigger an action when the page is closed."""
|
||||
|
||||
@property
|
||||
def _name_object(self) -> NameObject:
|
||||
"""Returns the corresponding NameObject for the trigger."""
|
||||
mapping = {
|
||||
PageTrigger.OPEN: NameObject("/O"),
|
||||
PageTrigger.CLOSE: NameObject("/C"),
|
||||
}
|
||||
return mapping[self]
|
||||
|
||||
|
||||
class Action(DictionaryObject, ABC):
|
||||
"""An action dictionary defines the characteristics and behaviour of an action."""
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self[NameObject("/Type")] = NameObject("/Action")
|
||||
# The next action or sequence of actions that shall be performed after the action
|
||||
# represented by this dictionary. The value is either a single action dictionary
|
||||
# or an array of action dictionaries that shall be performed in order.
|
||||
self[NameObject("/Next")] = NullObject() # Optional
|
||||
|
||||
@classmethod
|
||||
def _create_open_action(cls, writer: "PdfWriter", action: "Action") -> None:
|
||||
"""
|
||||
Create an action that shall be performed when the document is opened.
|
||||
|
||||
Args:
|
||||
writer: The writer to add the action to.
|
||||
action: The action to add.
|
||||
"""
|
||||
if "/Names" not in writer.root_object:
|
||||
writer.root_object[NameObject(CatalogAttributes.NAMES)] = DictionaryObject()
|
||||
|
||||
names = cast(DictionaryObject, writer.root_object[CatalogAttributes.NAMES])
|
||||
if "/JavaScript" not in names:
|
||||
names[NameObject("/JavaScript")] = DictionaryObject(
|
||||
{NameObject("/Names"): ArrayObject()}
|
||||
)
|
||||
|
||||
js = cast(
|
||||
ArrayObject, cast(DictionaryObject, names["/JavaScript"])["/Names"]
|
||||
)
|
||||
|
||||
# We need a name for parameterized JavaScript in the PDF file, but it can be anything
|
||||
js.append(create_string_object(str(uuid.uuid4())))
|
||||
js.append(writer._add_object(action))
|
||||
|
||||
@classmethod
|
||||
def _create_new(cls, page: "PageObject", trigger: PageTrigger, action: "Action") -> None:
|
||||
"""
|
||||
Create a new action and add it to the page.
|
||||
|
||||
Args:
|
||||
page: The page to add the action to.
|
||||
trigger: An open or close trigger.
|
||||
action: The action to be done.
|
||||
"""
|
||||
trigger_name = trigger._name_object
|
||||
|
||||
if "/AA" not in page:
|
||||
# Additional actions key not present
|
||||
page[NameObject("/AA")] = DictionaryObject(
|
||||
{trigger_name: action}
|
||||
)
|
||||
return
|
||||
|
||||
if isinstance(page["/AA"], NullObject):
|
||||
page[NameObject("/AA")] = DictionaryObject()
|
||||
|
||||
if not isinstance(page["/AA"].get_object(), DictionaryObject):
|
||||
current_type = type(page["/AA"]).__name__
|
||||
if page.pdf is not None and getattr(page.pdf, "strict", False):
|
||||
raise ParseError(
|
||||
f"The type in a page object's additional-actions key must be a DictionaryObject: "
|
||||
f"received type {current_type}."
|
||||
)
|
||||
logger_warning(
|
||||
"The type in a page object's additional-actions key must be a DictionaryObject: "
|
||||
"received type %(type)s.",
|
||||
source=__name__,
|
||||
type=current_type
|
||||
)
|
||||
return
|
||||
|
||||
additional_actions = cast(DictionaryObject, page["/AA"])
|
||||
|
||||
if is_null_or_none(additional_actions.get(trigger_name)):
|
||||
additional_actions.update({trigger_name: action})
|
||||
return
|
||||
|
||||
# The action dictionary's Next entry allows sequences of actions to be
|
||||
# chained together. For example, the effect of clicking a link
|
||||
# annotation with the mouse can be to play a sound, jump to a new
|
||||
# page, and start up a movie. Note that the Next entry is not
|
||||
# restricted to a single action but can contain an array of actions,
|
||||
# each of which in turn can have a Next entry of its own.
|
||||
# §12.6.2 Action dictionaries ISO 32000-2:2020
|
||||
head = current = additional_actions.get(trigger_name)
|
||||
if not isinstance(head, DictionaryObject):
|
||||
raise ParseError(
|
||||
f"The type in a page object's additional-actions key must be a DictionaryObject: "
|
||||
f"received type {type(head).__name__}."
|
||||
)
|
||||
current = cast(DictionaryObject, current)
|
||||
|
||||
visited = set()
|
||||
while True:
|
||||
next_node = current.get("/Next", None)
|
||||
|
||||
if is_null_or_none(next_node):
|
||||
break
|
||||
|
||||
if not isinstance(next_node, (ArrayObject, DictionaryObject)):
|
||||
raise TypeError(
|
||||
f"An action dictionary’s Next entry must be an Action dictionary "
|
||||
f"or an array of Action dictionaries: received type {type(next_node).__name__}."
|
||||
)
|
||||
|
||||
id_next = id(next_node)
|
||||
if id_next in visited:
|
||||
logger_warning("Detected cycle in the action tree for %(current)s", source=__name__, current=current)
|
||||
break
|
||||
visited.add(id_next)
|
||||
|
||||
if isinstance(next_node, ArrayObject):
|
||||
current = next_node[-1]
|
||||
else:
|
||||
current = next_node
|
||||
|
||||
if not is_null_or_none(next_node := current.get("/Next")) and id(next_node) in visited:
|
||||
logger_warning("Detected cycle in the action tree for %(current)s", source=__name__, current=current)
|
||||
|
||||
current[NameObject("/Next")] = action
|
||||
additional_actions.update({trigger_name: head})
|
||||
|
||||
@classmethod
|
||||
def _delete(cls, page: "PageObject", trigger: PageTrigger) -> None:
|
||||
"""
|
||||
Delete an action from the page.
|
||||
|
||||
Args:
|
||||
page: The page to delete the action.
|
||||
trigger: An open or close trigger.
|
||||
"""
|
||||
if "/AA" not in page:
|
||||
return
|
||||
|
||||
trigger_name = trigger._name_object
|
||||
|
||||
additional_actions = cast(DictionaryObject, page["/AA"])
|
||||
|
||||
if trigger_name not in additional_actions:
|
||||
return
|
||||
|
||||
del additional_actions[trigger_name]
|
||||
|
||||
if not additional_actions:
|
||||
del page["/AA"]
|
||||
|
||||
|
||||
class JavaScript(Action):
|
||||
"""
|
||||
Upon invocation of an ECMAScript action, a PDF processor shall execute a
|
||||
script that is written in the ECMAScript programming language. ECMAScript
|
||||
extensions described in ISO/DIS 21757-1 shall also be allowed.
|
||||
|
||||
Args:
|
||||
java_script: A text string containing the ECMAScript script to be executed.
|
||||
"""
|
||||
|
||||
def __init__(self, java_script: str) -> None:
|
||||
"""Initialize JavaScript with a string."""
|
||||
super().__init__()
|
||||
self[NameObject("/S")] = NameObject("/JavaScript")
|
||||
self[NameObject("/JS")] = TextStringObject(java_script)
|
||||
Reference in New Issue
Block a user