Update ashboard, dashboard, memory +1 more (+2 ~3)

This commit is contained in:
Echo
2026-02-02 22:27:24 +00:00
parent 4f00131184
commit b0c9b254f1
65 changed files with 42112 additions and 53 deletions

View File

@@ -0,0 +1,42 @@
"""
PDF specifies several annotation types which pypdf makes available here.
The names of the annotations and their attributes do not reflect the names in
the specification in all cases. For example, the PDF standard defines a
'Square' annotation that does not actually need to be square. For this reason,
pypdf calls it 'Rectangle'.
At their core, all annotation types are DictionaryObjects. That means if pypdf
does not implement a feature, users can easily extend the given functionality.
"""
from ._base import NO_FLAGS, AnnotationDictionary
from ._markup_annotations import (
Ellipse,
FreeText,
Highlight,
Line,
MarkupAnnotation,
Polygon,
PolyLine,
Rectangle,
Text,
)
from ._non_markup_annotations import Link, Popup
__all__ = [
"NO_FLAGS",
"AnnotationDictionary",
"Ellipse",
"FreeText",
"Highlight",
"Line",
"Link",
"MarkupAnnotation",
"PolyLine",
"Polygon",
"Popup",
"Rectangle",
"Text",
]

View File

@@ -0,0 +1,29 @@
from abc import ABC
from ..constants import AnnotationFlag
from ..generic import NameObject, NumberObject
from ..generic._data_structures import DictionaryObject
class AnnotationDictionary(DictionaryObject, ABC):
def __init__(self) -> None:
super().__init__()
from ..generic._base import NameObject # noqa: PLC0415
# /Rect should not be added here as Polygon and PolyLine can automatically set it
self[NameObject("/Type")] = NameObject("/Annot")
# The flags were NOT added to the constructor on purpose:
# We expect that most users don't want to change the default.
# If they do, they can use the property. The default is 0.
@property
def flags(self) -> AnnotationFlag:
return self.get(NameObject("/F"), AnnotationFlag(0))
@flags.setter
def flags(self, value: AnnotationFlag) -> None:
self[NameObject("/F")] = NumberObject(value)
NO_FLAGS = AnnotationFlag(0)

View File

@@ -0,0 +1,305 @@
import sys
from abc import ABC
from typing import Any, Optional, Union
from ..constants import AnnotationFlag
from ..generic import ArrayObject, DictionaryObject
from ..generic._base import (
BooleanObject,
FloatObject,
NameObject,
NumberObject,
TextStringObject,
)
from ..generic._rectangle import RectangleObject
from ..generic._utils import hex_to_rgb
from ._base import NO_FLAGS, AnnotationDictionary
if sys.version_info[:2] >= (3, 10):
from typing import TypeAlias
else:
# PEP 613 introduced typing.TypeAlias with Python 3.10
# For older Python versions, the backport typing_extensions is necessary:
from typing_extensions import TypeAlias
Vertex: TypeAlias = tuple[float, float]
def _get_bounding_rectangle(vertices: list[Vertex]) -> RectangleObject:
x_min, y_min = vertices[0][0], vertices[0][1]
x_max, y_max = vertices[0][0], vertices[0][1]
for x, y in vertices:
x_min = min(x_min, x)
y_min = min(y_min, y)
x_max = max(x_max, x)
y_max = max(y_max, y)
return RectangleObject((x_min, y_min, x_max, y_max))
class MarkupAnnotation(AnnotationDictionary, ABC):
"""
Base class for all markup annotations.
Args:
title_bar: Text to be displayed in the title bar of the annotation;
by convention this is the name of the author
"""
def __init__(self, *, title_bar: Optional[str] = None) -> None:
if title_bar is not None:
self[NameObject("/T")] = TextStringObject(title_bar)
class Text(MarkupAnnotation):
"""
A text annotation.
Args:
rect: array of four integers ``[xLL, yLL, xUR, yUR]``
specifying the clickable rectangular area
text: The text that is added to the document
open:
flags:
"""
def __init__(
self,
*,
rect: Union[RectangleObject, tuple[float, float, float, float]],
text: str,
open: bool = False,
flags: int = NO_FLAGS,
**kwargs: Any,
) -> None:
super().__init__(**kwargs)
self[NameObject("/Subtype")] = NameObject("/Text")
self[NameObject("/Rect")] = RectangleObject(rect)
self[NameObject("/Contents")] = TextStringObject(text)
self[NameObject("/Open")] = BooleanObject(open)
self[NameObject("/Flags")] = NumberObject(flags)
class FreeText(MarkupAnnotation):
"""A FreeText annotation"""
def __init__(
self,
*,
text: str,
rect: Union[RectangleObject, tuple[float, float, float, float]],
font: str = "Helvetica",
bold: bool = False,
italic: bool = False,
font_size: str = "14pt",
font_color: str = "000000",
border_color: Optional[str] = "000000",
background_color: Optional[str] = "ffffff",
**kwargs: Any,
) -> None:
super().__init__(**kwargs)
self[NameObject("/Subtype")] = NameObject("/FreeText")
self[NameObject("/Rect")] = RectangleObject(rect)
# Table 225 of the 1.7 reference ("CSS2 style attributes used in rich text strings")
font_str = "font: "
if italic:
font_str = f"{font_str}italic "
else:
font_str = f"{font_str}normal "
if bold:
font_str = f"{font_str}bold "
else:
font_str = f"{font_str}normal "
font_str = f"{font_str}{font_size} {font}"
font_str = f"{font_str};text-align:left;color:#{font_color}"
default_appearance_string = ""
if border_color:
for st in hex_to_rgb(border_color):
default_appearance_string = f"{default_appearance_string}{st} "
default_appearance_string = f"{default_appearance_string}rg"
self.update(
{
NameObject("/Subtype"): NameObject("/FreeText"),
NameObject("/Rect"): RectangleObject(rect),
NameObject("/Contents"): TextStringObject(text),
# font size color
NameObject("/DS"): TextStringObject(font_str),
NameObject("/DA"): TextStringObject(default_appearance_string),
}
)
if border_color is None:
# Border Style
self[NameObject("/BS")] = DictionaryObject(
{
# width of 0 means no border
NameObject("/W"): NumberObject(0)
}
)
if background_color is not None:
self[NameObject("/C")] = ArrayObject(
[FloatObject(n) for n in hex_to_rgb(background_color)]
)
class Line(MarkupAnnotation):
def __init__(
self,
p1: Vertex,
p2: Vertex,
rect: Union[RectangleObject, tuple[float, float, float, float]],
text: str = "",
**kwargs: Any,
) -> None:
super().__init__(**kwargs)
self.update(
{
NameObject("/Subtype"): NameObject("/Line"),
NameObject("/Rect"): RectangleObject(rect),
NameObject("/L"): ArrayObject(
[
FloatObject(p1[0]),
FloatObject(p1[1]),
FloatObject(p2[0]),
FloatObject(p2[1]),
]
),
NameObject("/LE"): ArrayObject(
[
NameObject("/None"),
NameObject("/None"),
]
),
NameObject("/IC"): ArrayObject(
[
FloatObject(0.5),
FloatObject(0.5),
FloatObject(0.5),
]
),
NameObject("/Contents"): TextStringObject(text),
}
)
class PolyLine(MarkupAnnotation):
def __init__(
self,
vertices: list[Vertex],
**kwargs: Any,
) -> None:
super().__init__(**kwargs)
if len(vertices) == 0:
raise ValueError("A polyline needs at least 1 vertex with two coordinates")
coord_list = []
for x, y in vertices:
coord_list.append(NumberObject(x))
coord_list.append(NumberObject(y))
self.update(
{
NameObject("/Subtype"): NameObject("/PolyLine"),
NameObject("/Vertices"): ArrayObject(coord_list),
NameObject("/Rect"): RectangleObject(_get_bounding_rectangle(vertices)),
}
)
class Rectangle(MarkupAnnotation):
def __init__(
self,
rect: Union[RectangleObject, tuple[float, float, float, float]],
*,
interior_color: Optional[str] = None,
**kwargs: Any,
) -> None:
super().__init__(**kwargs)
self.update(
{
NameObject("/Type"): NameObject("/Annot"),
NameObject("/Subtype"): NameObject("/Square"),
NameObject("/Rect"): RectangleObject(rect),
}
)
if interior_color:
self[NameObject("/IC")] = ArrayObject(
[FloatObject(n) for n in hex_to_rgb(interior_color)]
)
class Highlight(MarkupAnnotation):
def __init__(
self,
*,
rect: Union[RectangleObject, tuple[float, float, float, float]],
quad_points: ArrayObject,
highlight_color: str = "ff0000",
printing: bool = False,
**kwargs: Any,
) -> None:
super().__init__(**kwargs)
self.update(
{
NameObject("/Subtype"): NameObject("/Highlight"),
NameObject("/Rect"): RectangleObject(rect),
NameObject("/QuadPoints"): quad_points,
NameObject("/C"): ArrayObject(
[FloatObject(n) for n in hex_to_rgb(highlight_color)]
),
}
)
if printing:
self.flags = AnnotationFlag.PRINT
class Ellipse(MarkupAnnotation):
def __init__(
self,
rect: Union[RectangleObject, tuple[float, float, float, float]],
*,
interior_color: Optional[str] = None,
**kwargs: Any,
) -> None:
super().__init__(**kwargs)
self.update(
{
NameObject("/Type"): NameObject("/Annot"),
NameObject("/Subtype"): NameObject("/Circle"),
NameObject("/Rect"): RectangleObject(rect),
}
)
if interior_color:
self[NameObject("/IC")] = ArrayObject(
[FloatObject(n) for n in hex_to_rgb(interior_color)]
)
class Polygon(MarkupAnnotation):
def __init__(
self,
vertices: list[tuple[float, float]],
**kwargs: Any,
) -> None:
super().__init__(**kwargs)
if len(vertices) == 0:
raise ValueError("A polygon needs at least 1 vertex with two coordinates")
coord_list = []
for x, y in vertices:
coord_list.append(NumberObject(x))
coord_list.append(NumberObject(y))
self.update(
{
NameObject("/Type"): NameObject("/Annot"),
NameObject("/Subtype"): NameObject("/Polygon"),
NameObject("/Vertices"): ArrayObject(coord_list),
NameObject("/IT"): NameObject("/PolygonCloud"),
NameObject("/Rect"): RectangleObject(_get_bounding_rectangle(vertices)),
}
)

View File

@@ -0,0 +1,106 @@
from typing import TYPE_CHECKING, Any, Optional, Union
from ..generic._base import (
BooleanObject,
NameObject,
NumberObject,
TextStringObject,
)
from ..generic._data_structures import ArrayObject, DictionaryObject
from ..generic._fit import DEFAULT_FIT, Fit
from ..generic._rectangle import RectangleObject
from ._base import AnnotationDictionary
class Link(AnnotationDictionary):
def __init__(
self,
*,
rect: Union[RectangleObject, tuple[float, float, float, float]],
border: Optional[ArrayObject] = None,
url: Optional[str] = None,
target_page_index: Optional[int] = None,
fit: Fit = DEFAULT_FIT,
**kwargs: Any,
) -> None:
super().__init__(**kwargs)
if TYPE_CHECKING:
from ..types import BorderArrayType # noqa: PLC0415
is_external = url is not None
is_internal = target_page_index is not None
if not is_external and not is_internal:
raise ValueError(
"Either 'url' or 'target_page_index' have to be provided. Both were None."
)
if is_external and is_internal:
raise ValueError(
"Either 'url' or 'target_page_index' have to be provided. "
f"{url=}, {target_page_index=}"
)
border_arr: BorderArrayType
if border is not None:
border_arr = [NumberObject(n) for n in border[:3]]
if len(border) == 4:
dash_pattern = ArrayObject([NumberObject(n) for n in border[3]])
border_arr.append(dash_pattern)
else:
border_arr = [NumberObject(0)] * 3
self.update(
{
NameObject("/Type"): NameObject("/Annot"),
NameObject("/Subtype"): NameObject("/Link"),
NameObject("/Rect"): RectangleObject(rect),
NameObject("/Border"): ArrayObject(border_arr),
}
)
if is_external:
self[NameObject("/A")] = DictionaryObject(
{
NameObject("/S"): NameObject("/URI"),
NameObject("/Type"): NameObject("/Action"),
NameObject("/URI"): TextStringObject(url),
}
)
if is_internal:
# This needs to be updated later!
dest_deferred = DictionaryObject(
{
"target_page_index": NumberObject(target_page_index),
"fit": NameObject(fit.fit_type),
"fit_args": fit.fit_args,
}
)
self[NameObject("/Dest")] = dest_deferred
class Popup(AnnotationDictionary):
def __init__(
self,
*,
rect: Union[RectangleObject, tuple[float, float, float, float]],
parent: Optional[DictionaryObject] = None,
open: bool = False,
**kwargs: Any,
) -> None:
super().__init__(**kwargs)
self.update(
{
NameObject("/Subtype"): NameObject("/Popup"),
NameObject("/Rect"): RectangleObject(rect),
NameObject("/Open"): BooleanObject(open),
}
)
if parent:
# This needs to be an indirect object
try:
self[NameObject("/Parent")] = parent.indirect_reference
except AttributeError:
from .._utils import logger_warning # noqa: PLC0415
logger_warning(
"Unregistered Parent object : No Parent field set",
__name__,
)