codekingpro/portable-devtools
114k
1import sys2import uuid3from abc import ABC4from typing import Any, Literal, Optional, Union5 6from ..constants import AnnotationFlag7from ..generic import ArrayObject, DictionaryObject8from ..generic._base import (9 BooleanObject,10 FloatObject,11 IndirectObject,12 NameObject,13 NumberObject,14 TextStringObject,15)16from ..generic._rectangle import RectangleObject17from ..generic._utils import hex_to_rgb18from ._base import NO_FLAGS, AnnotationDictionary19 20if sys.version_info[:2] >= (3, 10):21 from typing import TypeAlias22else:23 # PEP 613 introduced typing.TypeAlias with Python 3.1024 # For older Python versions, the backport typing_extensions is necessary:25 from typing_extensions import TypeAlias26 27 28Vertex: TypeAlias = tuple[float, float]29 30 31def _get_bounding_rectangle(vertices: list[Vertex]) -> RectangleObject:32 x_min, y_min = vertices[0][0], vertices[0][1]33 x_max, y_max = vertices[0][0], vertices[0][1]34 for x, y in vertices:35 x_min = min(x_min, x)36 y_min = min(y_min, y)37 x_max = max(x_max, x)38 y_max = max(y_max, y)39 return RectangleObject((x_min, y_min, x_max, y_max))40 41 42class MarkupAnnotation(AnnotationDictionary, ABC):43 """44 Base class for all markup annotations.45 46 Args:47 title_bar: Text to be displayed in the title bar of the annotation;48 by convention this is the name of the author49 in_reply_to: The annotation that this annotation is "in reply to"50 (PDF 1.5). Can be either an annotation (previously added using51 :meth:`~pypdf.PdfWriter.add_annotation`) or a reference to the52 target annotation.53 reply_type: The relationship between this annotation and the one54 specified by ``in_reply_to``. Either ``"R"`` (a reply, default)55 or ``"Group"`` (grouped with the parent annotation). Raises56 ``ValueError`` if a non-default value is provided without57 ``in_reply_to``.58 annotation_name: A text string uniquely identifying this annotation59 among all annotations on its page. Automatically generated when60 ``in_reply_to`` is set and no name is provided. Raises61 ``ValueError`` if provided without ``in_reply_to``.62 63 """64 65 def __init__(66 self,67 *,68 title_bar: Optional[str] = None,69 in_reply_to: Optional[Union[DictionaryObject, IndirectObject]] = None,70 reply_type: Literal["R", "Group"] = "R",71 annotation_name: Optional[str] = None,72 ) -> None:73 if title_bar is not None:74 self[NameObject("/T")] = TextStringObject(title_bar)75 if annotation_name is not None and in_reply_to is None:76 raise ValueError(77 "annotation_name is only supported when in_reply_to is set"78 )79 if reply_type != "R" and in_reply_to is None:80 raise ValueError(81 "reply_type is only meaningful when in_reply_to is set"82 )83 if in_reply_to is not None:84 if isinstance(in_reply_to, IndirectObject):85 ref: IndirectObject = in_reply_to86 else:87 indirect_ref = getattr(in_reply_to, "indirect_reference", None)88 if not isinstance(indirect_ref, IndirectObject):89 raise ValueError(90 "in_reply_to must be a registered annotation "91 "(added via writer.add_annotation() first)"92 )93 ref = indirect_ref94 self[NameObject("/IRT")] = ref95 self[NameObject("/RT")] = NameObject(f"/{reply_type}")96 if annotation_name is None:97 annotation_name = str(uuid.uuid4())98 self[NameObject("/NM")] = TextStringObject(annotation_name)99 100 101class Text(MarkupAnnotation):102 """103 A text annotation.104 105 Args:106 rect: array of four integers ``[xLL, yLL, xUR, yUR]``107 specifying the clickable rectangular area108 text: The text that is added to the document109 open:110 flags:111 112 """113 114 def __init__(115 self,116 *,117 rect: Union[RectangleObject, tuple[float, float, float, float]],118 text: str,119 open: bool = False,120 flags: int = NO_FLAGS,121 **kwargs: Any,122 ) -> None:123 super().__init__(**kwargs)124 self[NameObject("/Subtype")] = NameObject("/Text")125 self[NameObject("/Rect")] = RectangleObject(rect)126 self[NameObject("/Contents")] = TextStringObject(text)127 self[NameObject("/Open")] = BooleanObject(open)128 self[NameObject("/Flags")] = NumberObject(flags)129 130 131class FreeText(MarkupAnnotation):132 """A FreeText annotation"""133 134 def __init__(135 self,136 *,137 text: str,138 rect: Union[RectangleObject, tuple[float, float, float, float]],139 font: str = "Helvetica",140 bold: bool = False,141 italic: bool = False,142 font_size: str = "14pt",143 font_color: str = "000000",144 border_color: Optional[str] = "000000",145 background_color: Optional[str] = "ffffff",146 **kwargs: Any,147 ) -> None:148 super().__init__(**kwargs)149 self[NameObject("/Subtype")] = NameObject("/FreeText")150 self[NameObject("/Rect")] = RectangleObject(rect)151 152 # Table 225 of the 1.7 reference ("CSS2 style attributes used in rich text strings")153 font_str = "font: "154 if italic:155 font_str = f"{font_str}italic "156 else:157 font_str = f"{font_str}normal "158 if bold:159 font_str = f"{font_str}bold "160 else:161 font_str = f"{font_str}normal "162 font_str = f"{font_str}{font_size} {font}"163 font_str = f"{font_str};text-align:left;color:#{font_color}"164 165 default_appearance_string = ""166 if border_color:167 for st in hex_to_rgb(border_color):168 default_appearance_string = f"{default_appearance_string}{st} "169 default_appearance_string = f"{default_appearance_string}rg"170 171 self.update(172 {173 NameObject("/Subtype"): NameObject("/FreeText"),174 NameObject("/Rect"): RectangleObject(rect),175 NameObject("/Contents"): TextStringObject(text),176 # font size color177 NameObject("/DS"): TextStringObject(font_str),178 NameObject("/DA"): TextStringObject(default_appearance_string),179 }180 )181 if border_color is None:182 # Border Style183 self[NameObject("/BS")] = DictionaryObject(184 {185 # width of 0 means no border186 NameObject("/W"): NumberObject(0)187 }188 )189 if background_color is not None:190 self[NameObject("/C")] = ArrayObject(191 [FloatObject(n) for n in hex_to_rgb(background_color)]192 )193 194 195class Line(MarkupAnnotation):196 def __init__(197 self,198 p1: Vertex,199 p2: Vertex,200 rect: Union[RectangleObject, tuple[float, float, float, float]],201 text: str = "",202 **kwargs: Any,203 ) -> None:204 super().__init__(**kwargs)205 self.update(206 {207 NameObject("/Subtype"): NameObject("/Line"),208 NameObject("/Rect"): RectangleObject(rect),209 NameObject("/L"): ArrayObject(210 [211 FloatObject(p1[0]),212 FloatObject(p1[1]),213 FloatObject(p2[0]),214 FloatObject(p2[1]),215 ]216 ),217 NameObject("/LE"): ArrayObject(218 [219 NameObject("/None"),220 NameObject("/None"),221 ]222 ),223 NameObject("/IC"): ArrayObject(224 [225 FloatObject(0.5),226 FloatObject(0.5),227 FloatObject(0.5),228 ]229 ),230 NameObject("/Contents"): TextStringObject(text),231 }232 )233 234 235class PolyLine(MarkupAnnotation):236 def __init__(237 self,238 vertices: list[Vertex],239 **kwargs: Any,240 ) -> None:241 super().__init__(**kwargs)242 if len(vertices) == 0:243 raise ValueError("A polyline needs at least 1 vertex with two coordinates")244 coord_list = []245 for x, y in vertices:246 coord_list.append(NumberObject(x))247 coord_list.append(NumberObject(y))248 self.update(249 {250 NameObject("/Subtype"): NameObject("/PolyLine"),251 NameObject("/Vertices"): ArrayObject(coord_list),252 NameObject("/Rect"): RectangleObject(_get_bounding_rectangle(vertices)),253 }254 )255 256 257class Rectangle(MarkupAnnotation):258 def __init__(259 self,260 rect: Union[RectangleObject, tuple[float, float, float, float]],261 *,262 interior_color: Optional[str] = None,263 **kwargs: Any,264 ) -> None:265 super().__init__(**kwargs)266 self.update(267 {268 NameObject("/Type"): NameObject("/Annot"),269 NameObject("/Subtype"): NameObject("/Square"),270 NameObject("/Rect"): RectangleObject(rect),271 }272 )273 274 if interior_color:275 self[NameObject("/IC")] = ArrayObject(276 [FloatObject(n) for n in hex_to_rgb(interior_color)]277 )278 279 280class Highlight(MarkupAnnotation):281 def __init__(282 self,283 *,284 rect: Union[RectangleObject, tuple[float, float, float, float]],285 quad_points: ArrayObject,286 highlight_color: str = "ff0000",287 printing: bool = False,288 **kwargs: Any,289 ) -> None:290 super().__init__(**kwargs)291 self.update(292 {293 NameObject("/Subtype"): NameObject("/Highlight"),294 NameObject("/Rect"): RectangleObject(rect),295 NameObject("/QuadPoints"): quad_points,296 NameObject("/C"): ArrayObject(297 [FloatObject(n) for n in hex_to_rgb(highlight_color)]298 ),299 }300 )301 if printing:302 self.flags = AnnotationFlag.PRINT303 304 305class Ellipse(MarkupAnnotation):306 def __init__(307 self,308 rect: Union[RectangleObject, tuple[float, float, float, float]],309 *,310 interior_color: Optional[str] = None,311 **kwargs: Any,312 ) -> None:313 super().__init__(**kwargs)314 315 self.update(316 {317 NameObject("/Type"): NameObject("/Annot"),318 NameObject("/Subtype"): NameObject("/Circle"),319 NameObject("/Rect"): RectangleObject(rect),320 }321 )322 323 if interior_color:324 self[NameObject("/IC")] = ArrayObject(325 [FloatObject(n) for n in hex_to_rgb(interior_color)]326 )327 328 329class Polygon(MarkupAnnotation):330 def __init__(331 self,332 vertices: list[tuple[float, float]],333 **kwargs: Any,334 ) -> None:335 super().__init__(**kwargs)336 if len(vertices) == 0:337 raise ValueError("A polygon needs at least 1 vertex with two coordinates")338 339 coord_list = []340 for x, y in vertices:341 coord_list.append(NumberObject(x))342 coord_list.append(NumberObject(y))343 self.update(344 {345 NameObject("/Type"): NameObject("/Annot"),346 NameObject("/Subtype"): NameObject("/Polygon"),347 NameObject("/Vertices"): ArrayObject(coord_list),348 NameObject("/IT"): NameObject("/PolygonCloud"),349 NameObject("/Rect"): RectangleObject(_get_bounding_rectangle(vertices)),350 }351 )352 