Line data Source code
1 : // Copyright 2026 The Authors.
2 : //
3 : // Licensed under the Apache License, Version 2.0 (the "License");
4 : // you may not use this file except in compliance with the License.
5 : // You may obtain a copy of the License at
6 : //
7 : // https://www.apache.org/licenses/LICENSE-2.0
8 : //
9 : // Unless required by applicable law or agreed to in writing, software
10 : // distributed under the License is distributed on an "AS IS" BASIS,
11 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 : // See the License for the specific language governing permissions and
13 : // limitations under the License.
14 :
15 : // Shared types for PDF document handling. These types have no platform
16 : // dependencies and are used by both the native (dart:ffi) and web (WASM)
17 : // backends. They represent the public API surface for metadata and error
18 : // handling.
19 :
20 : import 'dart:typed_data';
21 :
22 : // Compares two lists for deep equality using element-wise [==].
23 : //
24 : // Dart's built-in List equality is reference equality, so callers that hold
25 : // separate list instances with identical content would compare unequal without
26 : // this helper. Used by annotation types that carry [List] fields.
27 4 : bool _listEqual<T>(List<T> a, List<T> b) {
28 12 : if (a.length != b.length) return false;
29 10 : for (var i = 0; i < a.length; i++) {
30 12 : if (a[i] != b[i]) return false;
31 : }
32 : return true;
33 : }
34 :
35 : /// Errors that can occur during PDF document operations.
36 : enum PdfError {
37 : /// The document bytes are corrupt, not a valid PDF, or otherwise unloadable.
38 : invalidDocument,
39 :
40 : /// The document is password-protected. Passwords are not supported in v1;
41 : /// the caller should inform the user why the file could not be opened.
42 : passwordRequired,
43 : }
44 :
45 : /// Thrown when a PDF operation fails.
46 : ///
47 : /// The [error] field provides the reason for the failure. Callers should
48 : /// handle [PdfError.passwordRequired] and [PdfError.invalidDocument]
49 : /// separately to give users actionable error messages.
50 : ///
51 : /// Example:
52 : /// ```dart
53 : /// try {
54 : /// final doc = await PdfDocument.fromBytes(bytes);
55 : /// } on PdfExtractionException catch (e) {
56 : /// if (e.error == PdfError.passwordRequired) {
57 : /// // prompt user for a password
58 : /// } else {
59 : /// // report a corrupt or invalid file
60 : /// }
61 : /// }
62 : /// ```
63 : class PdfExtractionException implements Exception {
64 : /// Creates a [PdfExtractionException] with the given [error].
65 2 : const PdfExtractionException(this.error);
66 :
67 : /// The reason for the failure.
68 : final PdfError error;
69 :
70 1 : @override
71 3 : String toString() => 'PdfExtractionException(${error.name})';
72 : }
73 :
74 : /// A PDF date value, preserving both the raw string and the parsed [DateTime].
75 : ///
76 : /// PDF dates use the format `D:YYYYMMDDHHmmSSOHH'mm'` (where O is +, -, or Z).
77 : /// The `D:` prefix is optional in practice and the string may be truncated.
78 : /// When parsing fails, [value] is `null` but [raw] is always preserved so
79 : /// callers can inspect or log the original string.
80 : class PdfDate {
81 : /// Creates a [PdfDate] with the given raw string and parsed [DateTime].
82 5 : const PdfDate({required this.raw, required this.value});
83 :
84 : /// The raw date string as stored in the PDF Info dictionary.
85 : ///
86 : /// This value is always non-empty when the field is present. It follows the
87 : /// PDF date format `D:YYYYMMDDHHmmSSOHH'mm'` but real-world PDFs may deviate.
88 : final String raw;
89 :
90 : /// The parsed [DateTime], or `null` if [raw] could not be parsed.
91 : ///
92 : /// The returned [DateTime] is always in UTC (offset is converted). When the
93 : /// raw string contains an invalid or partial date, this field is `null` and
94 : /// [raw] is preserved for debugging purposes.
95 : final DateTime? value;
96 :
97 1 : @override
98 3 : String toString() => 'PdfDate(raw: $raw, value: $value)';
99 :
100 2 : @override
101 : bool operator ==(Object other) =>
102 : identical(this, other) ||
103 11 : other is PdfDate && raw == other.raw && value == other.value;
104 :
105 2 : @override
106 6 : int get hashCode => Object.hash(raw, value);
107 : }
108 :
109 : /// Metadata extracted from the PDF Info dictionary.
110 : ///
111 : /// All fields are nullable; a `null` value means the field was not present in
112 : /// the Info dictionary (as opposed to being present but empty). This mirrors
113 : /// the PDF specification where field presence and field value are distinct states.
114 : ///
115 : /// The eight standard Info dictionary fields are: [title], [author], [subject],
116 : /// [keywords], [creator], [producer], [creationDate], and [modDate].
117 : class PdfMetadata {
118 : /// Creates an immutable [PdfMetadata] value object.
119 3 : const PdfMetadata({
120 : this.title,
121 : this.author,
122 : this.subject,
123 : this.keywords,
124 : this.creator,
125 : this.producer,
126 : this.creationDate,
127 : this.modDate,
128 : });
129 :
130 : /// The document title, or `null` if not present.
131 : final String? title;
132 :
133 : /// The document author, or `null` if not present.
134 : final String? author;
135 :
136 : /// The document subject or description, or `null` if not present.
137 : final String? subject;
138 :
139 : /// Comma-separated keywords, or `null` if not present.
140 : final String? keywords;
141 :
142 : /// The application that created the original document, or `null` if not present.
143 : final String? creator;
144 :
145 : /// The application that converted the document to PDF, or `null` if not present.
146 : final String? producer;
147 :
148 : /// The date and time the document was created, or `null` if not present.
149 : final PdfDate? creationDate;
150 :
151 : /// The date and time the document was last modified, or `null` if not present.
152 : final PdfDate? modDate;
153 :
154 1 : @override
155 1 : String toString() =>
156 : 'PdfMetadata('
157 1 : 'title: $title, '
158 1 : 'author: $author, '
159 1 : 'subject: $subject, '
160 1 : 'keywords: $keywords, '
161 1 : 'creator: $creator, '
162 1 : 'producer: $producer, '
163 1 : 'creationDate: $creationDate, '
164 1 : 'modDate: $modDate'
165 : ')';
166 : }
167 :
168 : // ---------------------------------------------------------------------------
169 : // Text extraction types
170 : // ---------------------------------------------------------------------------
171 :
172 : /// The result of plain text extraction for a single PDF page.
173 : ///
174 : /// Produced by [PdfDocument.extractPlainText]. Each item in the stream
175 : /// corresponds to one page.
176 : ///
177 : /// [hasTextLayer] is the primary signal for whether useful text was extracted.
178 : /// Use [PdfDocument.isPlainTextExtractable] when you need a document-level
179 : /// heuristic rather than per-page signals.
180 : final class PdfPageText {
181 : /// Creates an immutable [PdfPageText] value.
182 3 : const PdfPageText({
183 : required this.pageIndex,
184 : required this.text,
185 : required this.hasUnicodeErrors,
186 : required this.hasTextLayer,
187 : });
188 :
189 : /// Zero-based index of the page this result corresponds to.
190 : final int pageIndex;
191 :
192 : /// The extracted Unicode text for this page.
193 : ///
194 : /// Soft hyphens (U+00AD) that appear at line-break positions are stripped
195 : /// and the surrounding words are joined. When [hasTextLayer] is false
196 : /// (scanned page), this is an empty string.
197 : final String text;
198 :
199 : /// True when at least one character on this page had a broken Unicode
200 : /// mapping (i.e. `FPDFText_HasUnicodeMapError` returned non-zero for it).
201 : ///
202 : /// Such characters are silently omitted from [text] by PDFium. This flag
203 : /// warns callers that the extracted text may be incomplete.
204 : final bool hasUnicodeErrors;
205 :
206 : /// True when PDFium extracted at least one character from this page.
207 : ///
208 : /// False indicates a scanned or image-only page with no text layer; [text]
209 : /// will be an empty string in that case.
210 : final bool hasTextLayer;
211 :
212 1 : @override
213 1 : String toString() =>
214 : 'PdfPageText('
215 1 : 'pageIndex: $pageIndex, '
216 1 : 'hasTextLayer: $hasTextLayer, '
217 1 : 'hasUnicodeErrors: $hasUnicodeErrors, '
218 7 : 'text: ${text.length > 40 ? '${text.substring(0, 40)}…' : text}'
219 : ')';
220 :
221 1 : @override
222 : bool operator ==(Object other) =>
223 : identical(this, other) ||
224 1 : other is PdfPageText &&
225 3 : pageIndex == other.pageIndex &&
226 3 : text == other.text &&
227 3 : hasUnicodeErrors == other.hasUnicodeErrors &&
228 3 : hasTextLayer == other.hasTextLayer;
229 :
230 1 : @override
231 : int get hashCode =>
232 5 : Object.hash(pageIndex, text, hasUnicodeErrors, hasTextLayer);
233 : }
234 :
235 : /// Configuration for text extraction heuristics.
236 : ///
237 : /// [scannedPageRatio] affects [PdfDocument.isPlainTextExtractable].
238 : /// [PdfPageText.hasTextLayer] is always determined by whether PDFium can
239 : /// extract any characters from the page — no configuration is needed.
240 : final class PdfTextExtractorConfig {
241 : /// Creates a [PdfTextExtractorConfig].
242 : ///
243 : /// [scannedPageRatio] must be > 0 and ≤ 1.
244 19 : const PdfTextExtractorConfig({this.scannedPageRatio = 0.5})
245 : : assert(
246 57 : scannedPageRatio > 0 && scannedPageRatio <= 1,
247 : 'scannedPageRatio must be > 0 and <= 1',
248 : );
249 :
250 : /// Fraction of pages that must have no text layer for
251 : /// [PdfDocument.isPlainTextExtractable] to return false. Default: 0.5.
252 : ///
253 : /// A value of 0.5 means a document is only considered predominantly scanned
254 : /// when more than half its pages yield no characters from PDFium. A single
255 : /// image or figure page in an otherwise text-based document will not trigger
256 : /// this flag.
257 : final double scannedPageRatio;
258 :
259 1 : @override
260 : String toString() =>
261 2 : 'PdfTextExtractorConfig(scannedPageRatio: $scannedPageRatio)';
262 : }
263 :
264 : // ---------------------------------------------------------------------------
265 : // Annotation types
266 : // ---------------------------------------------------------------------------
267 :
268 : /// The subtype of a PDF annotation, corresponding to the `fpdf_annot.h`
269 : /// `FPDF_ANNOT_*` constants.
270 : ///
271 : /// Only subtypes that are in scope for v0.02 are listed; form-field types
272 : /// (`FPDF_ANNOT_WIDGET`, `FPDF_ANNOT_XFAWIDGET`) are out of scope and are
273 : /// mapped to [unknown]. See `docs/spec/annotation_extraction.md` for details.
274 : enum PdfAnnotationType {
275 : /// Sticky note annotation (`FPDF_ANNOT_TEXT = 1`).
276 : text,
277 :
278 : /// Hyperlink annotation (`FPDF_ANNOT_LINK = 2`).
279 : link,
280 :
281 : /// Free-text (typewriter) annotation (`FPDF_ANNOT_FREETEXT = 3`).
282 : freeText,
283 :
284 : /// Line annotation (`FPDF_ANNOT_LINE = 4`).
285 : line,
286 :
287 : /// Rectangle annotation (`FPDF_ANNOT_SQUARE = 5`).
288 : square,
289 :
290 : /// Ellipse annotation (`FPDF_ANNOT_CIRCLE = 6`).
291 : circle,
292 :
293 : /// Polygon annotation (`FPDF_ANNOT_POLYGON = 7`).
294 : polygon,
295 :
296 : /// Polyline annotation (`FPDF_ANNOT_POLYLINE = 8`).
297 : polyline,
298 :
299 : /// Highlight annotation (`FPDF_ANNOT_HIGHLIGHT = 9`).
300 : highlight,
301 :
302 : /// Underline annotation (`FPDF_ANNOT_UNDERLINE = 10`).
303 : underline,
304 :
305 : /// Squiggly underline annotation (`FPDF_ANNOT_SQUIGGLY = 11`).
306 : squiggly,
307 :
308 : /// Strikeout annotation (`FPDF_ANNOT_STRIKEOUT = 12`).
309 : strikeout,
310 :
311 : /// Rubber stamp annotation (`FPDF_ANNOT_STAMP = 13`).
312 : stamp,
313 :
314 : /// Free-draw ink annotation (`FPDF_ANNOT_INK = 15`).
315 : ink,
316 :
317 : /// Popup annotation (`FPDF_ANNOT_POPUP = 16`). Inlined on parent, not emitted
318 : /// as a top-level annotation.
319 : popup,
320 :
321 : /// Any annotation subtype not recognised by this library version.
322 : unknown,
323 : }
324 :
325 : /// An ARGB colour value as extracted from a PDF annotation.
326 : ///
327 : /// Component values range from 0 to 255.
328 : final class PdfColor {
329 : /// Creates a [PdfColor] from its RGBA components.
330 2 : const PdfColor({
331 : required this.r,
332 : required this.g,
333 : required this.b,
334 : required this.a,
335 : });
336 :
337 : /// Red component, 0–255.
338 : final int r;
339 :
340 : /// Green component, 0–255.
341 : final int g;
342 :
343 : /// Blue component, 0–255.
344 : final int b;
345 :
346 : /// Alpha (opacity) component, 0–255 where 255 is fully opaque.
347 : final int a;
348 :
349 1 : @override
350 : bool operator ==(Object other) =>
351 : identical(this, other) ||
352 1 : other is PdfColor &&
353 3 : r == other.r &&
354 3 : g == other.g &&
355 3 : b == other.b &&
356 3 : a == other.a;
357 :
358 1 : @override
359 5 : int get hashCode => Object.hash(r, g, b, a);
360 :
361 1 : @override
362 5 : String toString() => 'PdfColor(r: $r, g: $g, b: $b, a: $a)';
363 : }
364 :
365 : /// A bounding rectangle in PDF page coordinates.
366 : ///
367 : /// PDFium uses a bottom-left page origin, so [bottom] < [top] in typical
368 : /// usage. Coordinates are in PDF user space units (points).
369 : ///
370 : /// Callers that need screen coordinates must apply `FPDF_PageToDevice()` /
371 : /// `FPDF_DeviceToPage()` themselves; this library exposes raw PDF coordinates.
372 : final class PdfRect {
373 : /// Creates a [PdfRect] from PDF page coordinates.
374 19 : const PdfRect({
375 : required this.left,
376 : required this.bottom,
377 : required this.right,
378 : required this.top,
379 : });
380 :
381 : /// Left edge in PDF user space (lower x value).
382 : final double left;
383 :
384 : /// Bottom edge in PDF user space (lower y value for bottom-left origin).
385 : final double bottom;
386 :
387 : /// Right edge in PDF user space (higher x value).
388 : final double right;
389 :
390 : /// Top edge in PDF user space (higher y value for bottom-left origin).
391 : final double top;
392 :
393 3 : @override
394 : bool operator ==(Object other) =>
395 : identical(this, other) ||
396 2 : other is PdfRect &&
397 6 : left == other.left &&
398 6 : bottom == other.bottom &&
399 3 : right == other.right &&
400 3 : top == other.top;
401 :
402 3 : @override
403 15 : int get hashCode => Object.hash(left, bottom, right, top);
404 :
405 2 : @override
406 : String toString() =>
407 10 : 'PdfRect(left: $left, bottom: $bottom, right: $right, top: $top)';
408 : }
409 :
410 : /// A point in PDF page coordinates.
411 : ///
412 : /// PDFium uses a bottom-left page origin. Coordinates are in PDF user space
413 : /// units (points).
414 : final class PdfPoint {
415 : /// Creates a [PdfPoint] at the given [x] and [y] coordinates.
416 3 : const PdfPoint({required this.x, required this.y});
417 :
418 : /// The x coordinate in PDF user space.
419 : final double x;
420 :
421 : /// The y coordinate in PDF user space.
422 : final double y;
423 :
424 2 : @override
425 : bool operator ==(Object other) =>
426 : identical(this, other) ||
427 11 : other is PdfPoint && x == other.x && y == other.y;
428 :
429 1 : @override
430 3 : int get hashCode => Object.hash(x, y);
431 :
432 2 : @override
433 6 : String toString() => 'PdfPoint(x: $x, y: $y)';
434 : }
435 :
436 : /// A set of four corner points defining one quadrilateral region.
437 : ///
438 : /// Used for text markup annotations (highlight, underline, squiggly,
439 : /// strikeout) to describe the precise area of marked-up text, which may not
440 : /// be axis-aligned. Points are ordered: top-left, top-right, bottom-left,
441 : /// bottom-right (following the PDF specification quad-point ordering).
442 : final class PdfQuadPoints {
443 : /// Creates a [PdfQuadPoints] from its four corner points.
444 2 : const PdfQuadPoints({
445 : required this.p1,
446 : required this.p2,
447 : required this.p3,
448 : required this.p4,
449 : });
450 :
451 : /// First point (top-left of the quadrilateral).
452 : final PdfPoint p1;
453 :
454 : /// Second point (top-right of the quadrilateral).
455 : final PdfPoint p2;
456 :
457 : /// Third point (bottom-left of the quadrilateral).
458 : final PdfPoint p3;
459 :
460 : /// Fourth point (bottom-right of the quadrilateral).
461 : final PdfPoint p4;
462 :
463 1 : @override
464 : bool operator ==(Object other) =>
465 : identical(this, other) ||
466 1 : other is PdfQuadPoints &&
467 3 : p1 == other.p1 &&
468 3 : p2 == other.p2 &&
469 3 : p3 == other.p3 &&
470 3 : p4 == other.p4;
471 :
472 1 : @override
473 5 : int get hashCode => Object.hash(p1, p2, p3, p4);
474 :
475 1 : @override
476 5 : String toString() => 'PdfQuadPoints(p1: $p1, p2: $p2, p3: $p3, p4: $p4)';
477 : }
478 :
479 : /// Popup annotation data inlined onto a parent annotation.
480 : ///
481 : /// `FPDF_ANNOT_POPUP` annotations are child annotations of sticky-note and
482 : /// free-text annotations. Rather than exposing them as top-level entries (which
483 : /// would confuse callers), the library inlines their data on the parent
484 : /// annotation as an optional [PdfPopupAnnotation] field.
485 : final class PdfPopupAnnotation {
486 : /// Creates a [PdfPopupAnnotation].
487 2 : const PdfPopupAnnotation({this.rect, required this.flags});
488 :
489 : /// The bounding rectangle of the popup window, or `null` if not available.
490 : final PdfRect? rect;
491 :
492 : /// Raw `FPDF_ANNOT_FLAG_*` bitmask for the popup annotation.
493 : final int flags;
494 :
495 1 : @override
496 : bool operator ==(Object other) =>
497 : identical(this, other) ||
498 7 : other is PdfPopupAnnotation && rect == other.rect && flags == other.flags;
499 :
500 1 : @override
501 3 : int get hashCode => Object.hash(rect, flags);
502 :
503 1 : @override
504 3 : String toString() => 'PdfPopupAnnotation(rect: $rect, flags: $flags)';
505 : }
506 :
507 : /// Base class for all PDF annotation types.
508 : ///
509 : /// Use a `switch` expression on the concrete subtype to access type-specific
510 : /// fields:
511 : ///
512 : /// ```dart
513 : /// switch (annotation) {
514 : /// PdfMarkupAnnotation(:final quadPoints, :final color) => ...,
515 : /// PdfLinkAnnotation(:final uri) => ...,
516 : /// _ => ...,
517 : /// }
518 : /// ```
519 : ///
520 : /// ## Common fields
521 : ///
522 : /// Every annotation carries [pageIndex], [flags], and optionally [rect],
523 : /// [color], [contents], [author], and [modifiedDate]. Type-specific fields
524 : /// live only on the appropriate subclass.
525 : ///
526 : /// ## Coordinate system
527 : ///
528 : /// [rect] is in raw PDF page coordinates (bottom-left origin). Callers that
529 : /// need screen coordinates must apply `FPDF_PageToDevice()` themselves.
530 : sealed class PdfAnnotation {
531 : /// Creates a [PdfAnnotation] with common fields.
532 3 : const PdfAnnotation({
533 : required this.pageIndex,
534 : this.contents,
535 : this.author,
536 : this.rect,
537 : this.color,
538 : this.modifiedDate,
539 : required this.flags,
540 : this.popup,
541 : });
542 :
543 : /// Zero-based index of the page this annotation belongs to.
544 : final int pageIndex;
545 :
546 : /// The annotation's `Contents` string, or `null` if absent.
547 : ///
548 : /// An empty string means the field is present but empty; `null` means the
549 : /// field is absent. These cases are intentionally distinguishable.
550 : final String? contents;
551 :
552 : /// The annotation author (`/T` dictionary entry), or `null` if absent.
553 : final String? author;
554 :
555 : /// The bounding rectangle in PDF page coordinates, or `null` if unavailable.
556 : final PdfRect? rect;
557 :
558 : /// The annotation colour, or `null` if no colour entry is present.
559 : final PdfColor? color;
560 :
561 : /// The last-modified date (`/M` dictionary entry), or `null` if absent.
562 : ///
563 : /// Parsed via `pdf_date_parser.dart`, consistent with [PdfMetadata.modDate].
564 : final PdfDate? modifiedDate;
565 :
566 : /// Raw `FPDF_ANNOT_FLAG_*` bitmask.
567 : ///
568 : /// See `FPDF_ANNOT_FLAG_HIDDEN`, `FPDF_ANNOT_FLAG_PRINT`, etc.
569 : final int flags;
570 :
571 : /// Inlined popup annotation data, or `null` if this annotation has no popup.
572 : ///
573 : /// Popup annotations (`FPDF_ANNOT_POPUP`) are child annotations of sticky
574 : /// notes and free-text annotations. They are not emitted as top-level
575 : /// entries; their data is inlined here instead.
576 : final PdfPopupAnnotation? popup;
577 : }
578 :
579 : /// A sticky note annotation (`FPDF_ANNOT_TEXT`).
580 : ///
581 : /// Sticky notes carry [contents], [author], [color], and optionally a [popup].
582 : final class PdfTextAnnotation extends PdfAnnotation {
583 : /// Creates a [PdfTextAnnotation].
584 3 : const PdfTextAnnotation({
585 : required super.pageIndex,
586 : super.contents,
587 : super.author,
588 : super.rect,
589 : super.color,
590 : super.modifiedDate,
591 : required super.flags,
592 : super.popup,
593 : });
594 :
595 1 : @override
596 : bool operator ==(Object other) =>
597 : identical(this, other) ||
598 1 : other is PdfTextAnnotation &&
599 3 : pageIndex == other.pageIndex &&
600 3 : contents == other.contents &&
601 3 : author == other.author &&
602 3 : rect == other.rect &&
603 3 : color == other.color &&
604 3 : modifiedDate == other.modifiedDate &&
605 3 : flags == other.flags &&
606 3 : popup == other.popup;
607 :
608 1 : @override
609 1 : int get hashCode => Object.hash(
610 1 : pageIndex,
611 1 : contents,
612 1 : author,
613 1 : rect,
614 1 : color,
615 1 : modifiedDate,
616 1 : flags,
617 1 : popup,
618 : );
619 :
620 1 : @override
621 1 : String toString() =>
622 2 : 'PdfTextAnnotation(pageIndex: $pageIndex, contents: $contents, '
623 3 : 'author: $author, rect: $rect, color: $color, '
624 3 : 'modifiedDate: $modifiedDate, flags: $flags, popup: $popup)';
625 : }
626 :
627 : /// A free-text (typewriter) annotation (`FPDF_ANNOT_FREETEXT`).
628 : final class PdfFreeTextAnnotation extends PdfAnnotation {
629 : /// Creates a [PdfFreeTextAnnotation].
630 2 : const PdfFreeTextAnnotation({
631 : required super.pageIndex,
632 : super.contents,
633 : super.author,
634 : super.rect,
635 : super.color,
636 : super.modifiedDate,
637 : required super.flags,
638 : super.popup,
639 : });
640 :
641 1 : @override
642 : bool operator ==(Object other) =>
643 : identical(this, other) ||
644 1 : other is PdfFreeTextAnnotation &&
645 3 : pageIndex == other.pageIndex &&
646 3 : contents == other.contents &&
647 3 : author == other.author &&
648 3 : rect == other.rect &&
649 3 : color == other.color &&
650 3 : modifiedDate == other.modifiedDate &&
651 3 : flags == other.flags &&
652 3 : popup == other.popup;
653 :
654 1 : @override
655 1 : int get hashCode => Object.hash(
656 1 : pageIndex,
657 1 : contents,
658 1 : author,
659 1 : rect,
660 1 : color,
661 1 : modifiedDate,
662 1 : flags,
663 1 : popup,
664 : );
665 :
666 1 : @override
667 1 : String toString() =>
668 2 : 'PdfFreeTextAnnotation(pageIndex: $pageIndex, contents: $contents, '
669 3 : 'author: $author, rect: $rect, color: $color, '
670 3 : 'modifiedDate: $modifiedDate, flags: $flags, popup: $popup)';
671 : }
672 :
673 : /// A text markup annotation: highlight, underline, squiggly, or strikeout.
674 : ///
675 : /// The [subtype] field distinguishes the four variants. [quadPoints] describes
676 : /// the exact region(s) of marked-up text; each element corresponds to one
677 : /// quadrilateral covering a line of text.
678 : final class PdfMarkupAnnotation extends PdfAnnotation {
679 : /// Creates a [PdfMarkupAnnotation].
680 2 : const PdfMarkupAnnotation({
681 : required super.pageIndex,
682 : required this.subtype,
683 : required this.quadPoints,
684 : this.markedText,
685 : super.contents,
686 : super.author,
687 : super.rect,
688 : super.color,
689 : super.modifiedDate,
690 : required super.flags,
691 : super.popup,
692 : });
693 :
694 : /// The markup subtype: [PdfAnnotationType.highlight], [PdfAnnotationType.underline],
695 : /// [PdfAnnotationType.squiggly], or [PdfAnnotationType.strikeout].
696 : final PdfAnnotationType subtype;
697 :
698 : /// Quad-point sets describing the marked-up text regions.
699 : ///
700 : /// Each element covers one line of text. An empty list means no quad-points
701 : /// were found (the bounding [rect] can still be used as a fallback).
702 : final List<PdfQuadPoints> quadPoints;
703 :
704 : /// The text covered by this markup annotation, extracted from the page's text layer.
705 : ///
706 : /// Null when the text page could not be loaded (e.g. the page has no text
707 : /// layer, as with scanned documents). An empty string means the text layer
708 : /// exists but no characters fall within the annotated region.
709 : final String? markedText;
710 :
711 1 : @override
712 : bool operator ==(Object other) =>
713 : identical(this, other) ||
714 1 : other is PdfMarkupAnnotation &&
715 3 : pageIndex == other.pageIndex &&
716 3 : subtype == other.subtype &&
717 3 : _listEqual(quadPoints, other.quadPoints) &&
718 3 : markedText == other.markedText &&
719 3 : contents == other.contents &&
720 3 : author == other.author &&
721 3 : rect == other.rect &&
722 3 : color == other.color &&
723 3 : modifiedDate == other.modifiedDate &&
724 3 : flags == other.flags &&
725 3 : popup == other.popup;
726 :
727 1 : @override
728 1 : int get hashCode => Object.hash(
729 1 : pageIndex,
730 1 : subtype,
731 2 : Object.hashAll(quadPoints),
732 1 : markedText,
733 1 : contents,
734 1 : author,
735 1 : rect,
736 1 : color,
737 1 : modifiedDate,
738 1 : flags,
739 1 : popup,
740 : );
741 :
742 1 : @override
743 1 : String toString() =>
744 2 : 'PdfMarkupAnnotation(pageIndex: $pageIndex, subtype: $subtype, '
745 3 : 'quadPoints: ${quadPoints.length} quads, markedText: $markedText, '
746 4 : 'contents: $contents, author: $author, color: $color, flags: $flags)';
747 : }
748 :
749 : /// A shape annotation: rectangle or ellipse.
750 : ///
751 : /// [subtype] is either [PdfAnnotationType.square] (rectangle) or
752 : /// [PdfAnnotationType.circle] (ellipse). [interiorColor] is the fill colour
753 : /// of the shape, distinct from the border [color].
754 : final class PdfShapeAnnotation extends PdfAnnotation {
755 : /// Creates a [PdfShapeAnnotation].
756 2 : const PdfShapeAnnotation({
757 : required super.pageIndex,
758 : required this.subtype,
759 : this.interiorColor,
760 : super.contents,
761 : super.author,
762 : super.rect,
763 : super.color,
764 : super.modifiedDate,
765 : required super.flags,
766 : super.popup,
767 : });
768 :
769 : /// The shape subtype: [PdfAnnotationType.square] or [PdfAnnotationType.circle].
770 : final PdfAnnotationType subtype;
771 :
772 : /// The fill (interior) colour, or `null` if not set.
773 : final PdfColor? interiorColor;
774 :
775 1 : @override
776 : bool operator ==(Object other) =>
777 : identical(this, other) ||
778 1 : other is PdfShapeAnnotation &&
779 3 : pageIndex == other.pageIndex &&
780 3 : subtype == other.subtype &&
781 3 : interiorColor == other.interiorColor &&
782 3 : contents == other.contents &&
783 3 : author == other.author &&
784 3 : rect == other.rect &&
785 3 : color == other.color &&
786 3 : modifiedDate == other.modifiedDate &&
787 3 : flags == other.flags &&
788 3 : popup == other.popup;
789 :
790 1 : @override
791 1 : int get hashCode => Object.hash(
792 1 : pageIndex,
793 1 : subtype,
794 1 : interiorColor,
795 1 : contents,
796 1 : author,
797 1 : rect,
798 1 : color,
799 1 : modifiedDate,
800 1 : flags,
801 1 : popup,
802 : );
803 :
804 1 : @override
805 1 : String toString() =>
806 2 : 'PdfShapeAnnotation(pageIndex: $pageIndex, subtype: $subtype, '
807 4 : 'interiorColor: $interiorColor, rect: $rect, color: $color, flags: $flags)';
808 : }
809 :
810 : /// A line annotation (`FPDF_ANNOT_LINE`).
811 : ///
812 : /// The line runs from [lineStart] to [lineEnd] in PDF page coordinates.
813 : final class PdfLineAnnotation extends PdfAnnotation {
814 : /// Creates a [PdfLineAnnotation].
815 2 : const PdfLineAnnotation({
816 : required super.pageIndex,
817 : required this.lineStart,
818 : required this.lineEnd,
819 : super.contents,
820 : super.author,
821 : super.rect,
822 : super.color,
823 : super.modifiedDate,
824 : required super.flags,
825 : super.popup,
826 : });
827 :
828 : /// The starting point of the line in PDF page coordinates.
829 : final PdfPoint lineStart;
830 :
831 : /// The ending point of the line in PDF page coordinates.
832 : final PdfPoint lineEnd;
833 :
834 1 : @override
835 : bool operator ==(Object other) =>
836 : identical(this, other) ||
837 1 : other is PdfLineAnnotation &&
838 3 : pageIndex == other.pageIndex &&
839 3 : lineStart == other.lineStart &&
840 3 : lineEnd == other.lineEnd &&
841 3 : contents == other.contents &&
842 3 : author == other.author &&
843 3 : rect == other.rect &&
844 3 : color == other.color &&
845 3 : modifiedDate == other.modifiedDate &&
846 3 : flags == other.flags &&
847 3 : popup == other.popup;
848 :
849 1 : @override
850 1 : int get hashCode => Object.hash(
851 1 : pageIndex,
852 1 : lineStart,
853 1 : lineEnd,
854 1 : contents,
855 1 : author,
856 1 : rect,
857 1 : color,
858 1 : modifiedDate,
859 1 : flags,
860 1 : popup,
861 : );
862 :
863 1 : @override
864 1 : String toString() =>
865 2 : 'PdfLineAnnotation(pageIndex: $pageIndex, lineStart: $lineStart, '
866 3 : 'lineEnd: $lineEnd, color: $color, flags: $flags)';
867 : }
868 :
869 : /// A free-draw ink annotation (`FPDF_ANNOT_INK`).
870 : ///
871 : /// [strokes] is a list of strokes; each stroke is a list of [PdfPoint]s
872 : /// forming a continuous path. Multiple strokes represent separate pen-down
873 : /// gestures.
874 : final class PdfInkAnnotation extends PdfAnnotation {
875 : /// Creates a [PdfInkAnnotation].
876 2 : const PdfInkAnnotation({
877 : required super.pageIndex,
878 : required this.strokes,
879 : super.contents,
880 : super.author,
881 : super.rect,
882 : super.color,
883 : super.modifiedDate,
884 : required super.flags,
885 : super.popup,
886 : });
887 :
888 : /// The list of ink strokes. Each inner list is one pen-down gesture.
889 : ///
890 : /// An empty outer list means no ink paths could be read (e.g. the annotation
891 : /// is present but contains no ink data). An inner list may be empty if a
892 : /// stroke has zero points.
893 : final List<List<PdfPoint>> strokes;
894 :
895 1 : @override
896 : bool operator ==(Object other) =>
897 : identical(this, other) ||
898 1 : other is PdfInkAnnotation &&
899 3 : pageIndex == other.pageIndex &&
900 3 : _strokesEqual(strokes, other.strokes) &&
901 3 : contents == other.contents &&
902 3 : author == other.author &&
903 3 : rect == other.rect &&
904 3 : color == other.color &&
905 3 : modifiedDate == other.modifiedDate &&
906 3 : flags == other.flags &&
907 3 : popup == other.popup;
908 :
909 : /// Deep-equality helper for the nested strokes list.
910 1 : static bool _strokesEqual(List<List<PdfPoint>> a, List<List<PdfPoint>> b) {
911 3 : if (a.length != b.length) return false;
912 3 : for (var i = 0; i < a.length; i++) {
913 5 : if (a[i].length != b[i].length) return false;
914 4 : for (var j = 0; j < a[i].length; j++) {
915 5 : if (a[i][j] != b[i][j]) return false;
916 : }
917 : }
918 : return true;
919 : }
920 :
921 1 : @override
922 1 : int get hashCode => Object.hash(
923 1 : pageIndex,
924 3 : Object.hashAll(strokes.map(Object.hashAll)),
925 1 : contents,
926 1 : author,
927 1 : rect,
928 1 : color,
929 1 : modifiedDate,
930 1 : flags,
931 1 : popup,
932 : );
933 :
934 1 : @override
935 1 : String toString() =>
936 3 : 'PdfInkAnnotation(pageIndex: $pageIndex, strokes: ${strokes.length}, '
937 2 : 'color: $color, flags: $flags)';
938 : }
939 :
940 : /// A polygon or polyline annotation.
941 : ///
942 : /// [subtype] is either [PdfAnnotationType.polygon] or
943 : /// [PdfAnnotationType.polyline]. [vertices] are the corner points.
944 : final class PdfPolygonAnnotation extends PdfAnnotation {
945 : /// Creates a [PdfPolygonAnnotation].
946 2 : const PdfPolygonAnnotation({
947 : required super.pageIndex,
948 : required this.subtype,
949 : required this.vertices,
950 : super.contents,
951 : super.author,
952 : super.rect,
953 : super.color,
954 : super.modifiedDate,
955 : required super.flags,
956 : super.popup,
957 : });
958 :
959 : /// The polygon/polyline subtype.
960 : final PdfAnnotationType subtype;
961 :
962 : /// The vertex points of the polygon or polyline, in order.
963 : final List<PdfPoint> vertices;
964 :
965 1 : @override
966 : bool operator ==(Object other) =>
967 : identical(this, other) ||
968 1 : other is PdfPolygonAnnotation &&
969 3 : pageIndex == other.pageIndex &&
970 3 : subtype == other.subtype &&
971 3 : _listEqual(vertices, other.vertices) &&
972 3 : contents == other.contents &&
973 3 : author == other.author &&
974 3 : rect == other.rect &&
975 3 : color == other.color &&
976 3 : modifiedDate == other.modifiedDate &&
977 3 : flags == other.flags &&
978 3 : popup == other.popup;
979 :
980 1 : @override
981 1 : int get hashCode => Object.hash(
982 1 : pageIndex,
983 1 : subtype,
984 2 : Object.hashAll(vertices),
985 1 : contents,
986 1 : author,
987 1 : rect,
988 1 : color,
989 1 : modifiedDate,
990 1 : flags,
991 1 : popup,
992 : );
993 :
994 1 : @override
995 1 : String toString() =>
996 2 : 'PdfPolygonAnnotation(pageIndex: $pageIndex, subtype: $subtype, '
997 4 : 'vertices: ${vertices.length}, color: $color, flags: $flags)';
998 : }
999 :
1000 : /// A link annotation (`FPDF_ANNOT_LINK`).
1001 : ///
1002 : /// Links carry either a [uri] (for URI actions) or a page destination (not
1003 : /// yet exposed — [uri] is `null` for non-URI actions). Callers should check
1004 : /// [uri] and filter appropriately; not all links have URI actions.
1005 : final class PdfLinkAnnotation extends PdfAnnotation {
1006 : /// Creates a [PdfLinkAnnotation].
1007 1 : const PdfLinkAnnotation({
1008 : required super.pageIndex,
1009 : this.uri,
1010 : super.contents,
1011 : super.author,
1012 : super.rect,
1013 : super.color,
1014 : super.modifiedDate,
1015 : required super.flags,
1016 : super.popup,
1017 : });
1018 :
1019 : /// The URI target of this link, or `null` if the link does not have a URI
1020 : /// action (e.g. it is a page-destination link or an unsupported action type).
1021 : final String? uri;
1022 :
1023 1 : @override
1024 : bool operator ==(Object other) =>
1025 : identical(this, other) ||
1026 1 : other is PdfLinkAnnotation &&
1027 3 : pageIndex == other.pageIndex &&
1028 3 : uri == other.uri &&
1029 3 : contents == other.contents &&
1030 3 : author == other.author &&
1031 3 : rect == other.rect &&
1032 3 : color == other.color &&
1033 3 : modifiedDate == other.modifiedDate &&
1034 3 : flags == other.flags &&
1035 3 : popup == other.popup;
1036 :
1037 1 : @override
1038 1 : int get hashCode => Object.hash(
1039 1 : pageIndex,
1040 1 : uri,
1041 1 : contents,
1042 1 : author,
1043 1 : rect,
1044 1 : color,
1045 1 : modifiedDate,
1046 1 : flags,
1047 1 : popup,
1048 : );
1049 :
1050 1 : @override
1051 1 : String toString() =>
1052 2 : 'PdfLinkAnnotation(pageIndex: $pageIndex, uri: $uri, '
1053 2 : 'rect: $rect, flags: $flags)';
1054 : }
1055 :
1056 : /// A rubber stamp annotation (`FPDF_ANNOT_STAMP`).
1057 : final class PdfStampAnnotation extends PdfAnnotation {
1058 : /// Creates a [PdfStampAnnotation].
1059 2 : const PdfStampAnnotation({
1060 : required super.pageIndex,
1061 : super.contents,
1062 : super.author,
1063 : super.rect,
1064 : super.color,
1065 : super.modifiedDate,
1066 : required super.flags,
1067 : super.popup,
1068 : });
1069 :
1070 1 : @override
1071 : bool operator ==(Object other) =>
1072 : identical(this, other) ||
1073 1 : other is PdfStampAnnotation &&
1074 3 : pageIndex == other.pageIndex &&
1075 3 : contents == other.contents &&
1076 3 : author == other.author &&
1077 3 : rect == other.rect &&
1078 3 : color == other.color &&
1079 3 : modifiedDate == other.modifiedDate &&
1080 3 : flags == other.flags &&
1081 3 : popup == other.popup;
1082 :
1083 1 : @override
1084 1 : int get hashCode => Object.hash(
1085 1 : pageIndex,
1086 1 : contents,
1087 1 : author,
1088 1 : rect,
1089 1 : color,
1090 1 : modifiedDate,
1091 1 : flags,
1092 1 : popup,
1093 : );
1094 :
1095 1 : @override
1096 1 : String toString() =>
1097 2 : 'PdfStampAnnotation(pageIndex: $pageIndex, contents: $contents, '
1098 2 : 'rect: $rect, flags: $flags)';
1099 : }
1100 :
1101 : /// An annotation whose subtype is not recognised by this library version.
1102 : ///
1103 : /// The [rawSubtype] field carries the original `FPDF_ANNOT_*` integer so
1104 : /// callers can inspect it for debugging or future-proofing purposes.
1105 : final class PdfUnknownAnnotation extends PdfAnnotation {
1106 : /// Creates a [PdfUnknownAnnotation].
1107 2 : const PdfUnknownAnnotation({
1108 : required super.pageIndex,
1109 : required this.rawSubtype,
1110 : super.contents,
1111 : super.author,
1112 : super.rect,
1113 : super.color,
1114 : super.modifiedDate,
1115 : required super.flags,
1116 : super.popup,
1117 : });
1118 :
1119 : /// The raw `FPDF_ANNOT_*` integer value that was not recognised.
1120 : final int rawSubtype;
1121 :
1122 1 : @override
1123 : bool operator ==(Object other) =>
1124 : identical(this, other) ||
1125 1 : other is PdfUnknownAnnotation &&
1126 3 : pageIndex == other.pageIndex &&
1127 3 : rawSubtype == other.rawSubtype &&
1128 3 : contents == other.contents &&
1129 3 : author == other.author &&
1130 3 : rect == other.rect &&
1131 3 : color == other.color &&
1132 3 : modifiedDate == other.modifiedDate &&
1133 3 : flags == other.flags &&
1134 3 : popup == other.popup;
1135 :
1136 1 : @override
1137 1 : int get hashCode => Object.hash(
1138 1 : pageIndex,
1139 1 : rawSubtype,
1140 1 : contents,
1141 1 : author,
1142 1 : rect,
1143 1 : color,
1144 1 : modifiedDate,
1145 1 : flags,
1146 1 : popup,
1147 : );
1148 :
1149 1 : @override
1150 1 : String toString() =>
1151 2 : 'PdfUnknownAnnotation(pageIndex: $pageIndex, rawSubtype: $rawSubtype, '
1152 1 : 'flags: $flags)';
1153 : }
1154 :
1155 : /// The annotations extracted from a single PDF page.
1156 : ///
1157 : /// Produced by [PdfDocument.extractAnnotations]. Each item in the stream
1158 : /// corresponds to one page. Pages with no annotations emit an entry with an
1159 : /// empty [annotations] list, so callers can track page coverage without gaps.
1160 : final class PdfPageAnnotations {
1161 : /// Creates an immutable [PdfPageAnnotations] value.
1162 2 : const PdfPageAnnotations({
1163 : required this.pageIndex,
1164 : required this.annotations,
1165 : });
1166 :
1167 : /// Zero-based index of the page this result corresponds to.
1168 : final int pageIndex;
1169 :
1170 : /// The annotations found on this page, in the order returned by PDFium.
1171 : ///
1172 : /// Popup annotations are inlined as [PdfAnnotation.popup] on their parent
1173 : /// and are not present as top-level entries in this list.
1174 : final List<PdfAnnotation> annotations;
1175 :
1176 1 : @override
1177 1 : String toString() =>
1178 1 : 'PdfPageAnnotations(pageIndex: $pageIndex, '
1179 2 : 'annotations: ${annotations.length})';
1180 : }
1181 :
1182 : // ---------------------------------------------------------------------------
1183 : // Table of contents types
1184 : // ---------------------------------------------------------------------------
1185 :
1186 : /// A single entry in the PDF bookmark/outline tree (Table of Contents).
1187 : ///
1188 : /// A PDF's "Outline" dictionary is its native Table of Contents structure.
1189 : /// Each entry has a [title], an optional destination ([pageIndex] and
1190 : /// [scrollPosition]), and an optional [uri] for URI-action entries.
1191 : /// [children] holds any nested sub-entries in the same tree shape.
1192 : ///
1193 : /// ## Destination resolution
1194 : ///
1195 : /// Bookmark destinations are resolved as follows:
1196 : /// - If the bookmark has a `PDFACTION_GOTO` action, [pageIndex] is the
1197 : /// zero-based page index and [scrollPosition] is the XYZ anchor (if any).
1198 : /// - If the bookmark has a `PDFACTION_URI` action, [uri] is the URI string and
1199 : /// [pageIndex] is `null`.
1200 : /// - If neither a matching action nor a direct destination is found, both
1201 : /// [pageIndex] and [uri] are `null` (section-label entry with no target).
1202 : ///
1203 : /// ## Zoom omission
1204 : ///
1205 : /// `FPDFDest_GetLocationInPage` returns an (x, y, zoom) triple for
1206 : /// `PDFDEST_VIEW_XYZ` destinations. The zoom value is intentionally **not**
1207 : /// surfaced here. Exposing zoom risks overriding the user's OS accessibility
1208 : /// zoom settings or Flutter's `textScaleFactor`, which would create a hostile
1209 : /// experience for users who rely on display magnification. Callers that need
1210 : /// precise magnification control should manage zoom independently of the
1211 : /// bookmark destination. Only the (x, y) scroll anchor is captured via
1212 : /// [scrollPosition].
1213 : final class PdfTocEntry {
1214 : /// Creates an immutable [PdfTocEntry].
1215 1 : const PdfTocEntry({
1216 : required this.title,
1217 : this.pageIndex,
1218 : this.uri,
1219 : this.scrollPosition,
1220 : this.children = const [],
1221 : });
1222 :
1223 : /// The display title of this bookmark entry.
1224 : ///
1225 : /// An empty string is valid — PDFium found a bookmark with no title text.
1226 : final String title;
1227 :
1228 : /// The zero-based page index this entry navigates to, or `null` if the
1229 : /// entry has no internal-page destination (e.g. it is a URI action or a
1230 : /// section label with no target).
1231 : ///
1232 : /// A value of `null` does not indicate an error; it indicates that this
1233 : /// entry either has a [uri] target or is a pure structural label.
1234 : final int? pageIndex;
1235 :
1236 : /// The URI this entry navigates to, or `null` if the entry is not a URI
1237 : /// action.
1238 : ///
1239 : /// Non-null only for bookmarks with a `PDFACTION_URI` action.
1240 : /// [pageIndex] is always `null` when [uri] is non-null.
1241 : final String? uri;
1242 :
1243 : /// The XYZ scroll anchor within the destination page, or `null` if either
1244 : /// the entry has no page destination or the destination does not carry
1245 : /// explicit position coordinates.
1246 : ///
1247 : /// Coordinates are in PDF user space (points, bottom-left origin), matching
1248 : /// the coordinate system used by [PdfRect] and [PdfPoint] throughout this
1249 : /// library. Callers that need screen-space coordinates must apply
1250 : /// `FPDF_PageToDevice()` themselves.
1251 : ///
1252 : /// See the class-level doc comment for why the zoom component of XYZ
1253 : /// destinations is not surfaced here.
1254 : final PdfPoint? scrollPosition;
1255 :
1256 : /// The child entries nested under this entry, in document order.
1257 : ///
1258 : /// An empty list means this is a leaf entry with no sub-items.
1259 : final List<PdfTocEntry> children;
1260 :
1261 1 : @override
1262 : bool operator ==(Object other) =>
1263 : identical(this, other) ||
1264 1 : other is PdfTocEntry &&
1265 3 : title == other.title &&
1266 3 : pageIndex == other.pageIndex &&
1267 3 : uri == other.uri &&
1268 3 : scrollPosition == other.scrollPosition &&
1269 3 : _listEqual(children, other.children);
1270 :
1271 1 : @override
1272 1 : int get hashCode => Object.hash(
1273 1 : title,
1274 1 : pageIndex,
1275 1 : uri,
1276 1 : scrollPosition,
1277 2 : Object.hashAll(children),
1278 : );
1279 :
1280 1 : @override
1281 1 : String toString() =>
1282 3 : 'PdfTocEntry(title: $title, pageIndex: $pageIndex, uri: $uri, '
1283 3 : 'scrollPosition: $scrollPosition, children: ${children.length})';
1284 : }
1285 :
1286 : // ---------------------------------------------------------------------------
1287 : // Image extraction types
1288 : // ---------------------------------------------------------------------------
1289 :
1290 : /// The colorspace of a PDF image object, corresponding to the
1291 : /// `FPDF_COLORSPACE_*` constants in `fpdf_edit.h`.
1292 : ///
1293 : /// Raw PDFium integer constants are not exposed in the public API; all
1294 : /// colorspaces are mapped to this enum. Use [unknown] as the fallback for
1295 : /// any value not recognised by this version of the library.
1296 : enum PdfColorspace {
1297 : /// `FPDF_COLORSPACE_UNKNOWN = 0` — colorspace not identified.
1298 : unknown,
1299 :
1300 : /// `FPDF_COLORSPACE_DEVICEGRAY = 1` — single-channel grey.
1301 : deviceGray,
1302 :
1303 : /// `FPDF_COLORSPACE_DEVICERGB = 2` — additive RGB.
1304 : deviceRgb,
1305 :
1306 : /// `FPDF_COLORSPACE_DEVICECMYK = 3` — subtractive CMYK.
1307 : deviceCmyk,
1308 :
1309 : /// `FPDF_COLORSPACE_CALGRAY = 4` — calibrated greyscale.
1310 : calGray,
1311 :
1312 : /// `FPDF_COLORSPACE_CALRGB = 5` — calibrated RGB.
1313 : calRgb,
1314 :
1315 : /// `FPDF_COLORSPACE_LAB = 6` — CIE L*a*b*.
1316 : lab,
1317 :
1318 : /// `FPDF_COLORSPACE_ICCBASED = 7` — ICC profile-based colorspace.
1319 : iccBased,
1320 :
1321 : /// `FPDF_COLORSPACE_SEPARATION = 8` — separation (spot colour).
1322 : separation,
1323 :
1324 : /// `FPDF_COLORSPACE_DEVICEN = 9` — DeviceN (multi-ink).
1325 : deviceN,
1326 :
1327 : /// `FPDF_COLORSPACE_INDEXED = 10` — indexed / palette.
1328 : indexed,
1329 :
1330 : /// `FPDF_COLORSPACE_PATTERN = 11` — pattern colorspace.
1331 : pattern,
1332 : }
1333 :
1334 : /// Source-level metadata for a single image object in a PDF page.
1335 : ///
1336 : /// These values come directly from the `FPDF_IMAGEOBJ_METADATA` struct and
1337 : /// describe the image as stored in the PDF (before any transforms are applied).
1338 : /// The rendered output may differ in dimensions — see [PdfImage.bitmapWidth]
1339 : /// and [PdfImage.bitmapHeight].
1340 : ///
1341 : /// [markedContentId] links the image to the document's structure tree for
1342 : /// alt-text lookup via `fpdf_structtree.h`. A value of `-1` means no
1343 : /// marked-content identifier is present.
1344 : final class PdfImageMetadata {
1345 : /// Creates an immutable [PdfImageMetadata].
1346 2 : const PdfImageMetadata({
1347 : required this.width,
1348 : required this.height,
1349 : required this.horizontalDpi,
1350 : required this.verticalDpi,
1351 : required this.bitsPerPixel,
1352 : required this.colorspace,
1353 : required this.markedContentId,
1354 : });
1355 :
1356 : /// Source pixel width of the image as stored in the PDF.
1357 : final int width;
1358 :
1359 : /// Source pixel height of the image as stored in the PDF.
1360 : final int height;
1361 :
1362 : /// Horizontal resolution in dots per inch.
1363 : final double horizontalDpi;
1364 :
1365 : /// Vertical resolution in dots per inch.
1366 : final double verticalDpi;
1367 :
1368 : /// Bits per pixel of the source image data (e.g. 1, 8, 24).
1369 : ///
1370 : /// A value of 1 typically indicates an image mask (stencil). Image mask
1371 : /// objects appear in the [PdfPageImages.images] list and are not suppressed
1372 : /// automatically — callers can identify them via this field.
1373 : final int bitsPerPixel;
1374 :
1375 : /// The colorspace of the source image data.
1376 : final PdfColorspace colorspace;
1377 :
1378 : /// The marked-content identifier linking this image to the structure tree,
1379 : /// or `-1` if the image has no marked-content entry.
1380 : ///
1381 : /// Callers that need the associated alt-text must look up this identifier
1382 : /// in the document structure tree via `fpdf_structtree.h` independently.
1383 : final int markedContentId;
1384 :
1385 2 : @override
1386 : bool operator ==(Object other) =>
1387 : identical(this, other) ||
1388 2 : other is PdfImageMetadata &&
1389 6 : width == other.width &&
1390 3 : height == other.height &&
1391 3 : horizontalDpi == other.horizontalDpi &&
1392 3 : verticalDpi == other.verticalDpi &&
1393 3 : bitsPerPixel == other.bitsPerPixel &&
1394 3 : colorspace == other.colorspace &&
1395 3 : markedContentId == other.markedContentId;
1396 :
1397 2 : @override
1398 2 : int get hashCode => Object.hash(
1399 2 : width,
1400 2 : height,
1401 2 : horizontalDpi,
1402 2 : verticalDpi,
1403 2 : bitsPerPixel,
1404 2 : colorspace,
1405 2 : markedContentId,
1406 : );
1407 :
1408 2 : @override
1409 2 : String toString() =>
1410 : 'PdfImageMetadata('
1411 4 : 'width: $width, height: $height, '
1412 4 : 'horizontalDpi: $horizontalDpi, verticalDpi: $verticalDpi, '
1413 4 : 'bitsPerPixel: $bitsPerPixel, colorspace: $colorspace, '
1414 2 : 'markedContentId: $markedContentId)';
1415 : }
1416 :
1417 : /// A single image object on a PDF page.
1418 : ///
1419 : /// Produced by [PdfDocument.extractImages]. [objectIndex] is the stable
1420 : /// per-page integer position of this object in the page's object list; pass it
1421 : /// to [PdfDocument.renderImage] to fetch the bitmap on demand.
1422 : ///
1423 : /// ## Bitmap fields
1424 : ///
1425 : /// [bgra], [bitmapWidth], and [bitmapHeight] are `null` when
1426 : /// [PdfDocument.extractImages] was called with `includeBitmap: false` (the
1427 : /// default, metadata-only mode). Use [PdfDocument.renderImage] to retrieve the
1428 : /// composited BGRA bitmap for a specific image without re-enumerating the page.
1429 : ///
1430 : /// When `includeBitmap: true` is passed, all three fields are populated for
1431 : /// every image that has a renderable bitmap; they remain `null` for mask-only
1432 : /// or otherwise unrenderable objects (when `FPDFImageObj_GetRenderedBitmap`
1433 : /// returns null).
1434 : ///
1435 : /// ## Image masks
1436 : ///
1437 : /// Image mask objects (`bits_per_pixel == 1`) are included in the output and
1438 : /// are not suppressed automatically. Callers can identify them via
1439 : /// `metadata.bitsPerPixel == 1`.
1440 : final class PdfImage {
1441 : /// Creates an immutable [PdfImage].
1442 2 : const PdfImage({
1443 : required this.pageIndex,
1444 : required this.objectIndex,
1445 : required this.metadata,
1446 : required this.bounds,
1447 : required this.filters,
1448 : this.bgra,
1449 : this.bitmapWidth,
1450 : this.bitmapHeight,
1451 : });
1452 :
1453 : /// Zero-based index of the page this image belongs to.
1454 : final int pageIndex;
1455 :
1456 : /// Position of this object in the page's object list (zero-based).
1457 : ///
1458 : /// This index is stable for the lifetime of the open document and can be
1459 : /// passed directly to [PdfDocument.renderImage] to fetch the BGRA bitmap
1460 : /// on demand.
1461 : final int objectIndex;
1462 :
1463 : /// Source-level metadata for this image (dimensions, DPI, colorspace).
1464 : final PdfImageMetadata metadata;
1465 :
1466 : /// Axis-aligned bounding box of the image in PDF user-space coordinates.
1467 : ///
1468 : /// Coordinates use the PDF bottom-left origin. The box reflects the image's
1469 : /// position and scaling on the page after all transforms are applied.
1470 : final PdfRect bounds;
1471 :
1472 : /// The compression filter names applied to the image data, in order.
1473 : ///
1474 : /// For example, `['DCTDecode']` indicates JPEG encoding, and
1475 : /// `['FlateDecode']` indicates zlib/deflate. An empty list means no filters
1476 : /// were found (or the image uses an inline/uncompressed format).
1477 : final List<String> filters;
1478 :
1479 : /// The rendered BGRA pixel bytes, or `null` if the bitmap was not requested.
1480 : ///
1481 : /// Non-null only when [PdfDocument.extractImages] was called with
1482 : /// `includeBitmap: true` and `FPDFImageObj_GetRenderedBitmap` succeeded.
1483 : /// The byte length equals [bitmapWidth]! * [bitmapHeight]! * 4.
1484 : final Uint8List? bgra;
1485 :
1486 : /// The rendered pixel width, or `null` if the bitmap was not requested.
1487 : ///
1488 : /// May differ from [PdfImageMetadata.width] after transforms are applied.
1489 : final int? bitmapWidth;
1490 :
1491 : /// The rendered pixel height, or `null` if the bitmap was not requested.
1492 : ///
1493 : /// May differ from [PdfImageMetadata.height] after transforms are applied.
1494 : final int? bitmapHeight;
1495 :
1496 2 : @override
1497 : bool operator ==(Object other) =>
1498 : identical(this, other) ||
1499 2 : other is PdfImage &&
1500 6 : pageIndex == other.pageIndex &&
1501 6 : objectIndex == other.objectIndex &&
1502 6 : metadata == other.metadata &&
1503 6 : bounds == other.bounds &&
1504 6 : _listEqual(filters, other.filters) &&
1505 6 : bitmapWidth == other.bitmapWidth &&
1506 6 : bitmapHeight == other.bitmapHeight;
1507 : // bgra is intentionally excluded from equality to avoid comparing large
1508 : // byte buffers by value; callers that need bitmap equality should compare
1509 : // the bgra lists directly.
1510 :
1511 2 : @override
1512 2 : int get hashCode => Object.hash(
1513 2 : pageIndex,
1514 2 : objectIndex,
1515 2 : metadata,
1516 2 : bounds,
1517 4 : Object.hashAll(filters),
1518 2 : bitmapWidth,
1519 2 : bitmapHeight,
1520 : );
1521 :
1522 2 : @override
1523 2 : String toString() =>
1524 : 'PdfImage('
1525 4 : 'pageIndex: $pageIndex, objectIndex: $objectIndex, '
1526 4 : 'metadata: $metadata, bounds: $bounds, '
1527 2 : 'filters: $filters, '
1528 4 : 'bitmapWidth: $bitmapWidth, bitmapHeight: $bitmapHeight, '
1529 5 : 'bgra: ${bgra != null ? '${bgra!.length} bytes' : 'null'})';
1530 : }
1531 :
1532 : /// A rendered bitmap returned by [PdfDocument.renderImage].
1533 : ///
1534 : /// [bgra] is the composited BGRA pixel buffer produced by
1535 : /// `FPDFImageObj_GetRenderedBitmap`. The rendering includes mask composition
1536 : /// and transform application, so the output dimensions ([width] × [height])
1537 : /// may differ from the source image dimensions in [PdfImageMetadata].
1538 : final class PdfImageBitmap {
1539 : /// Creates an immutable [PdfImageBitmap].
1540 2 : const PdfImageBitmap({
1541 : required this.bgra,
1542 : required this.width,
1543 : required this.height,
1544 : });
1545 :
1546 : /// Rendered BGRA pixel bytes. Length equals [width] * [height] * 4.
1547 : final Uint8List bgra;
1548 :
1549 : /// Rendered pixel width.
1550 : final int width;
1551 :
1552 : /// Rendered pixel height.
1553 : final int height;
1554 :
1555 2 : @override
1556 : bool operator ==(Object other) =>
1557 : identical(this, other) ||
1558 14 : other is PdfImageBitmap && width == other.width && height == other.height;
1559 : // bgra is intentionally excluded from equality; compare lists directly if
1560 : // pixel-exact equality is required.
1561 :
1562 2 : @override
1563 6 : int get hashCode => Object.hash(width, height);
1564 :
1565 2 : @override
1566 2 : String toString() =>
1567 4 : 'PdfImageBitmap(width: $width, height: $height, '
1568 4 : 'bgra: ${bgra.length} bytes)';
1569 : }
1570 :
1571 : /// The image objects extracted from a single PDF page.
1572 : ///
1573 : /// Produced by [PdfDocument.extractImages]. Each item in the stream
1574 : /// corresponds to one page. Pages with no image objects emit an entry with an
1575 : /// empty [images] list, so callers can track page coverage without gaps.
1576 : final class PdfPageImages {
1577 : /// Creates an immutable [PdfPageImages] value.
1578 2 : const PdfPageImages({required this.pageIndex, required this.images});
1579 :
1580 : /// Zero-based index of the page this result corresponds to.
1581 : final int pageIndex;
1582 :
1583 : /// The image objects found on this page, in object-list order.
1584 : ///
1585 : /// Includes image mask objects (`metadata.bitsPerPixel == 1`); these are
1586 : /// not suppressed automatically. An empty list means the page has no image
1587 : /// objects.
1588 : final List<PdfImage> images;
1589 :
1590 2 : @override
1591 : String toString() =>
1592 8 : 'PdfPageImages(pageIndex: $pageIndex, images: ${images.length})';
1593 : }
1594 :
1595 : // ---------------------------------------------------------------------------
1596 : // Search types
1597 : // ---------------------------------------------------------------------------
1598 :
1599 : /// Flags that control the text-search behaviour of [PdfDocument.search].
1600 : ///
1601 : /// Combine flags using a [Set]:
1602 : ///
1603 : /// ```dart
1604 : /// final matches = doc.search('example',
1605 : /// flags: {PdfSearchFlag.matchCase, PdfSearchFlag.matchWholeWord});
1606 : /// ```
1607 : ///
1608 : /// Flag values correspond to the PDFium `FPDF_MATCHCASE`,
1609 : /// `FPDF_MATCHWHOLEWORD`, and `FPDF_CONSECUTIVE` constants defined in
1610 : /// `fpdf_text.h`.
1611 : enum PdfSearchFlag {
1612 : /// Case-sensitive matching (`FPDF_MATCHCASE = 0x00000001`).
1613 : ///
1614 : /// When set, "Apple" does not match "apple". When absent the search is
1615 : /// case-insensitive.
1616 : matchCase,
1617 :
1618 : /// Whole-word matching (`FPDF_MATCHWHOLEWORD = 0x00000002`).
1619 : ///
1620 : /// When set, "art" does not match the substring "art" inside "artist".
1621 : matchWholeWord,
1622 :
1623 : /// Allow overlapping / consecutive matches (`FPDF_CONSECUTIVE = 0x00000004`).
1624 : ///
1625 : /// When set, searching for "aa" in "aaa" produces two overlapping matches
1626 : /// (at index 0 and index 1). When absent each match starts immediately after
1627 : /// the previous match ends.
1628 : consecutive,
1629 : }
1630 :
1631 : /// A single text-search match returned by [PdfDocument.search].
1632 : ///
1633 : /// Each instance describes one occurrence of the search query on a specific
1634 : /// page. Multi-line matches (where the matching text wraps across visual rows)
1635 : /// produce a single [PdfSearchMatch] with multiple entries in [rects] — one
1636 : /// per visual line fragment.
1637 : ///
1638 : /// ## Coordinate system
1639 : ///
1640 : /// All coordinates in [rects] are in **PDF user space** (origin bottom-left,
1641 : /// units in points), consistent with [PdfRect] and page-size coordinates used
1642 : /// throughout this library. Callers that need screen-space coordinates must
1643 : /// apply `FPDF_PageToDevice()` / `FPDF_DeviceToPage()` themselves.
1644 : final class PdfSearchMatch {
1645 : /// Creates an immutable [PdfSearchMatch].
1646 1 : const PdfSearchMatch({
1647 : required this.pageIndex,
1648 : required this.charIndex,
1649 : required this.charCount,
1650 : required this.rects,
1651 : });
1652 :
1653 : /// Zero-based index of the page on which this match was found.
1654 : final int pageIndex;
1655 :
1656 : /// Zero-based character index of the first matched character on this page.
1657 : ///
1658 : /// This index is relative to the page's text layer, consistent with the
1659 : /// character indices used by `FPDFText_GetText` and `FPDFText_GetCharBox`.
1660 : final int charIndex;
1661 :
1662 : /// Number of matched characters.
1663 : ///
1664 : /// The matched text spans characters `[charIndex, charIndex + charCount)`.
1665 : final int charCount;
1666 :
1667 : /// Bounding rectangles of this match in PDF user-space (origin bottom-left,
1668 : /// units in points).
1669 : ///
1670 : /// A match that spans a single visual line produces one rect. A match that
1671 : /// wraps across multiple visual rows produces one rect per row fragment.
1672 : /// Callers should treat all rects as fragments that together cover the full
1673 : /// extent of this match.
1674 : ///
1675 : /// Uses [PdfRect] — the same coordinate space as page sizes and annotation
1676 : /// bounding boxes throughout this library.
1677 : final List<PdfRect> rects;
1678 :
1679 1 : @override
1680 : bool operator ==(Object other) =>
1681 : identical(this, other) ||
1682 1 : other is PdfSearchMatch &&
1683 3 : pageIndex == other.pageIndex &&
1684 3 : charIndex == other.charIndex &&
1685 3 : charCount == other.charCount &&
1686 3 : _listEqual(rects, other.rects);
1687 :
1688 1 : @override
1689 : int get hashCode =>
1690 6 : Object.hash(pageIndex, charIndex, charCount, Object.hashAll(rects));
1691 :
1692 1 : @override
1693 1 : String toString() =>
1694 : 'PdfSearchMatch('
1695 1 : 'pageIndex: $pageIndex, '
1696 1 : 'charIndex: $charIndex, '
1697 1 : 'charCount: $charCount, '
1698 2 : 'rects: ${rects.length})';
1699 : }
1700 :
1701 : // ---------------------------------------------------------------------------
1702 : // Thumbnail types
1703 : // ---------------------------------------------------------------------------
1704 :
1705 : /// Whether a [PdfThumbnail] came from an embedded stream or was rendered.
1706 : ///
1707 : /// Embedded thumbnails are smaller than rendered ones (typically 64–256 px)
1708 : /// and are returned at their native size. Rendered thumbnails are produced on
1709 : /// demand by the rendering engine at the caller-controlled `maxDimension`.
1710 : enum PdfThumbnailSource {
1711 : /// Decoded from an embedded `/Thumb` stream in the PDF page dictionary.
1712 : ///
1713 : /// Not all PDFs contain embedded thumbnails. Modern tools like `pdflatex`
1714 : /// and many web-based PDF creators do not produce `/Thumb` streams. When a
1715 : /// thumbnail is embedded, [PdfDocument.getThumbnail] returns it at its
1716 : /// native dimensions without any scaling.
1717 : embedded,
1718 :
1719 : /// Rendered from the page content at [PdfDocument.getThumbnail]'s
1720 : /// `maxDimension` because no embedded thumbnail was present.
1721 : ///
1722 : /// The rendered thumbnail is produced by the same rendering engine as
1723 : /// [PdfDocument.renderPageToBytes]. On high-DPI displays, multiply
1724 : /// `maxDimension` by the device pixel ratio before calling to obtain a
1725 : /// retina-sharp result.
1726 : rendered,
1727 : }
1728 :
1729 : /// A thumbnail image for a PDF page.
1730 : ///
1731 : /// Obtain via [PdfDocument.getThumbnail]. Pixel data is in BGRA format
1732 : /// (4 bytes per pixel, blue first). The [source] field indicates whether
1733 : /// the thumbnail was decoded from an embedded stream or synthesised by
1734 : /// rendering the page.
1735 : ///
1736 : /// ## Pixel format
1737 : ///
1738 : /// [bgra] is always a compact BGRA buffer: `length == width * height * 4`.
1739 : /// The bytes are ordered B, G, R, A per pixel, row-major. Row padding from
1740 : /// the underlying PDFium bitmap is stripped before delivery.
1741 : ///
1742 : /// ## Embedded vs rendered thumbnails
1743 : ///
1744 : /// Embedded thumbnails ([PdfThumbnailSource.embedded]) are stored directly in
1745 : /// the PDF and returned at whatever dimensions the authoring tool chose.
1746 : /// Rendered thumbnails ([PdfThumbnailSource.rendered]) respect `maxDimension`.
1747 : ///
1748 : /// Example — converting a [PdfThumbnail] to a Flutter `dart:ui Image`:
1749 : ///
1750 : /// ```dart
1751 : /// final thumb = await doc.getThumbnail(0);
1752 : /// if (thumb != null) {
1753 : /// final codec = await ui.instantiateImageCodec(
1754 : /// thumb.bgra,
1755 : /// targetWidth: thumb.width,
1756 : /// targetHeight: thumb.height,
1757 : /// );
1758 : /// final frame = await codec.getNextFrame();
1759 : /// final image = frame.image;
1760 : /// }
1761 : /// ```
1762 : final class PdfThumbnail {
1763 : /// Creates an immutable [PdfThumbnail].
1764 1 : const PdfThumbnail({
1765 : required this.bgra,
1766 : required this.width,
1767 : required this.height,
1768 : required this.source,
1769 : });
1770 :
1771 : /// BGRA pixel bytes. Length is always [width] * [height] * 4.
1772 : ///
1773 : /// Bytes are ordered B, G, R, A per pixel, row-major. Row padding from the
1774 : /// underlying PDFium bitmap has been stripped; the buffer is compact.
1775 : final Uint8List bgra;
1776 :
1777 : /// Width of the thumbnail in pixels.
1778 : final int width;
1779 :
1780 : /// Height of the thumbnail in pixels.
1781 : final int height;
1782 :
1783 : /// Whether this thumbnail was decoded from an embedded stream or rendered.
1784 : final PdfThumbnailSource source;
1785 :
1786 1 : @override
1787 : bool operator ==(Object other) =>
1788 : identical(this, other) ||
1789 1 : other is PdfThumbnail &&
1790 3 : width == other.width &&
1791 3 : height == other.height &&
1792 3 : source == other.source;
1793 : // bgra is intentionally excluded from equality to avoid comparing large
1794 : // byte buffers by value. Callers that need pixel-exact equality should
1795 : // compare the bgra lists directly.
1796 :
1797 1 : @override
1798 4 : int get hashCode => Object.hash(width, height, source);
1799 :
1800 1 : @override
1801 1 : String toString() =>
1802 3 : 'PdfThumbnail(width: $width, height: $height, source: $source, '
1803 2 : 'bgra: ${bgra.length} bytes)';
1804 : }
1805 :
1806 : // ---------------------------------------------------------------------------
1807 : // Document info types
1808 : // ---------------------------------------------------------------------------
1809 :
1810 : /// Document-level properties that are distinct from content metadata.
1811 : ///
1812 : /// These are low-level PDF document properties: the PDF file version and the
1813 : /// two file identifier entries (permanent and changing). They are returned as
1814 : /// a single batched call to avoid multiple isolate round-trips.
1815 : ///
1816 : /// File identifiers are typically 16-byte MD5 hashes. They are returned as
1817 : /// [Uint8List] (raw bytes) so callers can choose their own encoding (e.g.
1818 : /// hex string via `hex.encode()`).
1819 : class PdfDocumentInfo {
1820 : /// Creates an immutable [PdfDocumentInfo] value object.
1821 3 : const PdfDocumentInfo({this.fileVersion, this.permanentId, this.changingId});
1822 :
1823 : /// The PDF file version as an integer (e.g. 17 for PDF 1.7), or `null` if
1824 : /// the version could not be read.
1825 : final int? fileVersion;
1826 :
1827 : /// The permanent file identifier (typically a 16-byte MD5 hash), or `null`
1828 : /// if the document has no file identifier array.
1829 : ///
1830 : /// The permanent ID is set when the document is first created and does not
1831 : /// change across save operations. Callers that need a hex string can use
1832 : /// `permanentId?.map((b) => b.toRadixString(16).padLeft(2, '0')).join()`.
1833 : final Uint8List? permanentId;
1834 :
1835 : /// The changing file identifier (typically a 16-byte MD5 hash), or `null`
1836 : /// if the document has no file identifier array.
1837 : ///
1838 : /// The changing ID is updated on each save. Together with [permanentId] it
1839 : /// can be used to detect whether a file is a revision of a known document.
1840 : final Uint8List? changingId;
1841 :
1842 2 : @override
1843 : String toString() {
1844 2 : String? hexEncode(Uint8List? bytes) =>
1845 10 : bytes?.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
1846 2 : return 'PdfDocumentInfo('
1847 2 : 'fileVersion: $fileVersion, '
1848 4 : 'permanentId: ${hexEncode(permanentId)}, '
1849 4 : 'changingId: ${hexEncode(changingId)}'
1850 : ')';
1851 : }
1852 : }
|