blitz-pdf 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/README.md +266 -12
  2. package/THIRD_PARTY_NOTICES.md +4 -2
  3. package/dist/browser.d.ts +1 -1
  4. package/dist/browser.d.ts.map +1 -1
  5. package/dist/document-options.d.ts +36 -0
  6. package/dist/document-options.d.ts.map +1 -0
  7. package/dist/document-options.js +236 -0
  8. package/dist/document-options.js.map +1 -0
  9. package/dist/node.d.ts +1 -1
  10. package/dist/node.d.ts.map +1 -1
  11. package/dist/options.d.ts +35 -1
  12. package/dist/options.d.ts.map +1 -1
  13. package/dist/options.js +21 -14
  14. package/dist/options.js.map +1 -1
  15. package/dist/types.d.ts +71 -1
  16. package/dist/types.d.ts.map +1 -1
  17. package/dist/validation.d.ts +2 -0
  18. package/dist/validation.d.ts.map +1 -0
  19. package/dist/validation.js +7 -0
  20. package/dist/validation.js.map +1 -0
  21. package/examples/zugferd.mjs +41 -0
  22. package/licenses/RUST_DEPENDENCIES.txt +27 -0
  23. package/package.json +3 -2
  24. package/src/browser.ts +1 -1
  25. package/src/document-options.ts +218 -0
  26. package/src/node.ts +1 -1
  27. package/src/options.ts +18 -13
  28. package/src/types.ts +76 -1
  29. package/src/validation.ts +6 -0
  30. package/vendor/README.md +27 -2
  31. package/vendor/krilla/.cargo-ok +1 -0
  32. package/vendor/krilla/.cargo_vcs_info.json +6 -0
  33. package/vendor/krilla/Cargo.lock +1357 -0
  34. package/vendor/krilla/Cargo.toml +165 -0
  35. package/vendor/krilla/LICENSE_APACHE +202 -0
  36. package/vendor/krilla/LICENSE_MIT +21 -0
  37. package/vendor/krilla/README.md +156 -0
  38. package/vendor/krilla/examples/basic.rs +117 -0
  39. package/vendor/krilla/examples/empty_document.rs +25 -0
  40. package/vendor/krilla/examples/parley.rs +174 -0
  41. package/vendor/krilla/examples/simple_text.rs +118 -0
  42. package/vendor/krilla/examples/stream_builder.rs +93 -0
  43. package/vendor/krilla/icc/LICENSE +116 -0
  44. package/vendor/krilla/icc/README.md +1 -0
  45. package/vendor/krilla/icc/sGrey-v2-magic.icc +0 -0
  46. package/vendor/krilla/icc/sGrey-v4.icc +0 -0
  47. package/vendor/krilla/icc/sRGB-v2-magic.icc +0 -0
  48. package/vendor/krilla/icc/sRGB-v4.icc +0 -0
  49. package/vendor/krilla/src/chunk_container.rs +503 -0
  50. package/vendor/krilla/src/configure/PDF_A1.md +227 -0
  51. package/vendor/krilla/src/configure/PDF_A2.md +269 -0
  52. package/vendor/krilla/src/configure/PDF_A3.md +31 -0
  53. package/vendor/krilla/src/configure/PDF_A4.md +211 -0
  54. package/vendor/krilla/src/configure/PDF_UA1.md +202 -0
  55. package/vendor/krilla/src/configure/README.md +23 -0
  56. package/vendor/krilla/src/configure/mod.rs +197 -0
  57. package/vendor/krilla/src/configure/validate.rs +1244 -0
  58. package/vendor/krilla/src/configure/version.rs +134 -0
  59. package/vendor/krilla/src/content.rs +1352 -0
  60. package/vendor/krilla/src/data.rs +49 -0
  61. package/vendor/krilla/src/document.rs +156 -0
  62. package/vendor/krilla/src/error.rs +170 -0
  63. package/vendor/krilla/src/geom.rs +341 -0
  64. package/vendor/krilla/src/graphics/blend.rs +46 -0
  65. package/vendor/krilla/src/graphics/color.rs +552 -0
  66. package/vendor/krilla/src/graphics/graphic.rs +30 -0
  67. package/vendor/krilla/src/graphics/graphics_state.rs +251 -0
  68. package/vendor/krilla/src/graphics/icc.rs +218 -0
  69. package/vendor/krilla/src/graphics/image.rs +835 -0
  70. package/vendor/krilla/src/graphics/mask.rs +128 -0
  71. package/vendor/krilla/src/graphics/mod.rs +14 -0
  72. package/vendor/krilla/src/graphics/paint.rs +434 -0
  73. package/vendor/krilla/src/graphics/separation.rs +86 -0
  74. package/vendor/krilla/src/graphics/shading_function.rs +865 -0
  75. package/vendor/krilla/src/graphics/shading_pattern.rs +60 -0
  76. package/vendor/krilla/src/graphics/tiling_pattern.rs +119 -0
  77. package/vendor/krilla/src/graphics/xobject.rs +150 -0
  78. package/vendor/krilla/src/interactive/action.rs +68 -0
  79. package/vendor/krilla/src/interactive/annotation.rs +283 -0
  80. package/vendor/krilla/src/interactive/destination.rs +289 -0
  81. package/vendor/krilla/src/interactive/mod.rs +3 -0
  82. package/vendor/krilla/src/interchange/embed.rs +240 -0
  83. package/vendor/krilla/src/interchange/metadata.rs +564 -0
  84. package/vendor/krilla/src/interchange/mod.rs +4 -0
  85. package/vendor/krilla/src/interchange/outline.rs +263 -0
  86. package/vendor/krilla/src/interchange/tagging/fmt.rs +733 -0
  87. package/vendor/krilla/src/interchange/tagging/generate.toml +474 -0
  88. package/vendor/krilla/src/interchange/tagging/generated.rs +4754 -0
  89. package/vendor/krilla/src/interchange/tagging/mod.rs +1324 -0
  90. package/vendor/krilla/src/interchange/tagging/tag.rs +686 -0
  91. package/vendor/krilla/src/lib.rs +179 -0
  92. package/vendor/krilla/src/num.rs +26 -0
  93. package/vendor/krilla/src/page.rs +612 -0
  94. package/vendor/krilla/src/pdf.rs +313 -0
  95. package/vendor/krilla/src/resource.rs +436 -0
  96. package/vendor/krilla/src/serialize.rs +1101 -0
  97. package/vendor/krilla/src/stream.rs +368 -0
  98. package/vendor/krilla/src/surface.rs +664 -0
  99. package/vendor/krilla/src/text/cid.rs +488 -0
  100. package/vendor/krilla/src/text/font.rs +351 -0
  101. package/vendor/krilla/src/text/glyph/bitmap.rs +81 -0
  102. package/vendor/krilla/src/text/glyph/colr.rs +581 -0
  103. package/vendor/krilla/src/text/glyph/mod.rs +205 -0
  104. package/vendor/krilla/src/text/glyph/outline.rs +72 -0
  105. package/vendor/krilla/src/text/glyph/svg.rs +34 -0
  106. package/vendor/krilla/src/text/group.rs +463 -0
  107. package/vendor/krilla/src/text/mod.rs +232 -0
  108. package/vendor/krilla/src/text/shape.rs +102 -0
  109. package/vendor/krilla/src/text/type3.rs +514 -0
  110. package/vendor/krilla/src/util.rs +286 -0
  111. package/vendor/stylo/properties/longhands.toml +0 -1
  112. package/vendor/stylo/stylesheets/rule_parser.rs +1 -1
  113. package/wasm/blitz_pdf.js +10 -5
  114. package/wasm/blitz_pdf_bg.wasm +0 -0
@@ -0,0 +1,1324 @@
1
+ //! Creating accessible PDF documents.
2
+ //!
3
+ //! # Introduction
4
+ //!
5
+ //! A document usually consists of many smaller semantic building blocks, like for example
6
+ //! titles, headings, paragraphs, tables, headers, footers, and so on. However, it is important
7
+ //! to understand that, by default, when exporting a document to PDF, all of this semantic
8
+ //! information is lost.
9
+ //!
10
+ //! By default, a PDF document doesn't have a notion of a table or a
11
+ //! paragraph. Instead, it consists of very low-level instructions, such as "draw
12
+ //! a path at that location" or "draw a line of glyphs at that font size". A table is
13
+ //! not encoded as a "table", but instead as a number of rectangle-like paths that happen
14
+ //! to surround lines of text. This is what made PDF popular in the first place, as encoding
15
+ //! information at such a low level allows to ensure a consistent viewing experience across
16
+ //! different platforms and viewers.
17
+ //!
18
+ //! However, this design has drawbacks, one of the main ones being that it leads to the
19
+ //! production of non-accessible documents. Especially in recent years, ensuring the
20
+ //! accessibility of documents has become an increasingly important requirement.
21
+ //! To address this deficiency, PDF introduced the notion of "tagged PDF", which consists
22
+ //! of enriching PDF documents with additional semantic information in a way that can be
23
+ //! interpreted by different consumers. krilla supports the creation of such documents.
24
+ //!
25
+ //! # A word on krilla's implementation
26
+ //!
27
+ //! As nearly everything in PDF, tagging is a really complex topic, and getting it right
28
+ //! is very hard. Because of this, in line with the general philosophy of krilla, some
29
+ //! of the potential capabilities of tagged PDF are not directly supported. Instead, only
30
+ //! a specific subset has been implemented, with a focus on features that improve the
31
+ //! accessibility of documents.
32
+ //!
33
+ //! Please note that doing tagging 100% correctly as recommended by various PDF standards is very
34
+ //! difficult. While the documentation lists many of the recommendations to abstract away
35
+ //! as much as possible, users are still expected to consult the given specifications to
36
+ //! ensure their implementation matches the specification. A "reference implementation" that
37
+ //! could be good to consult is the [`typst-pdf`](https://github.com/typst/typst/tree/main/crates/typst-pdf)
38
+ //! crate which implements most of the expected features for well-tagged PDFs.
39
+ //!
40
+ //! # Basic Principles
41
+ //!
42
+ //! The way tagged PDFs are created is by attaching a tag tree to the PDF document
43
+ //! that encodes the logical structure of the document. As mentioned above, a raw PDF file
44
+ //! mainly consists of text- and path-drawing primitives, which are not necessarily
45
+ //! drawn in the logical reading order of the document. What the tag tree does is
46
+ //! maping the different "snippets" of the PDF file to the tree-like structure in a way
47
+ //! that reflects the logical structure of the document, in reading-order.
48
+ //!
49
+ //! For example, a document can consist of multiple "sections", where each section might contain
50
+ //! headings, paragraphs or figures. A figure might consist of a table as well as a caption.
51
+ //! A table consists yet again of smaller semantic components, like a header, footer
52
+ //! and the data cells, which usually contain some text. These kinds of hierarchical structures
53
+ //! can be encoded with the help of tagging.
54
+ //!
55
+ //! A tag tree consists of two components:
56
+ //! - Group nodes, which represents a component with certain semantics. A group node must have
57
+ //! at least one child, otherwise it's discarded.
58
+ //! - Leaf nodes, which represent the actual pieces on the page that form part of a group.
59
+ //!
60
+ //! # How to create a tagged document
61
+ //!
62
+ //! If you want to create a tagged document, you need to follow the following steps:
63
+ //!
64
+ //! 1) Ensure that you activate the `enable_tagging` attribute in [`SerializeSettings`].
65
+ //! 2) Create a [tag tree](TagTree), which represents the "root" of a tag tree.
66
+ //! 3) As you create your document, create new [tag groups](TagGroup) with corresponding [tags](Tag).
67
+ //! Nest them with other tag groups, if necessary, by using the `push` method.
68
+ //! 4) Populate tag groups with [identifiers](Identifier), which represent the leaf nodes
69
+ //! in the tag tree. Identifiers are unique and point to a sequence of content on the
70
+ //! page. If you push an identifier to a tag group, then all content that is marked by
71
+ //! that identifier belongs semantically to that tag group. There are currently two ways
72
+ //! of obtaining an identifier:
73
+ //!
74
+ //! - Use the `add_tagged_annotation` method on [`Page`], which allows you to associate
75
+ //! annotations to the content they correspond to. Currently, krilla only supports link
76
+ //! annotations, and a link annotation should always be a child in a tag group with the
77
+ //! [`TagKind`] [`Link`](TagKind::Link), with its sibling being an identifier or another tag group that is
78
+ //! to be associated with the link.
79
+ //! - Use the `start_tagged` command on [`Surface`], which returns an [Identifier], and
80
+ //! indicates that all content drawn on the surface should be associated with that
81
+ //! identifier, until you call the `end_tagged` method. *Important*: Note that you cannot
82
+ //! nest calls to `start_tagged`, and you have to ensure that you always call a corresponding
83
+ //! `end_tagged`. Otherwise, krilla will panic.
84
+ //!
85
+ //! It is very important that each identifier you create has exactly one parent in the tag
86
+ //! tree. This means that you cannot create an identifier and not use it at all (0 parents),
87
+ //! or use the same identifier in two different parts of the tree (1+ parents). Otherwise,
88
+ //! export will fail.
89
+ //!
90
+ //! 5) Once you have built your tag tree, simply call `set_tag_tree` on [`Document`]. That's it!
91
+ //!
92
+ //! # Other notes
93
+ //!
94
+ //! Make sure that you carefully read the documentation of the other parts of this module, as
95
+ //! there are some more points as well as best practices you need to keep in mind
96
+ //! when creating well-tagged documents. The PDF specification is in some places very vague
97
+ //! on how a tagged document should look like, so there is quite a bit of ambiguity.
98
+ //!
99
+ //! Apart from that, the PDF specification does make a few statements on requirements a well-tagged PDF
100
+ //! should follows, although those are not really "strict" requirements in the sense that they can
101
+ //! be automatically checked by a PDF validator, so not conforming to some of those points does not
102
+ //! suddenly make your document a badly-tagged document! However, if possible, you should still
103
+ //! try to comply with the following requirements:
104
+ //!
105
+ //! - In general, all contents in your file should be tagged, either as an artifact or with
106
+ //! Span/Other.
107
+ //! - The order of elements in the tag tree should represent the logical reading order, including
108
+ //! annotations.
109
+ //! - Word breaks in text should be represented explicitly with spaces, instead of implicitly
110
+ //! by not including them, but instead positioning text in a way that "simulates" the spaces.
111
+ //! - Hyphenation should be represented as a soft hyphen character (U+00AD) instead
112
+ //! of a hard hyphen (U+002D).
113
+ //! - Tag groups should follow the best-practice of what kind of children they contain. See
114
+ //! [`TagKind`] for more information.
115
+ //! - You should provide "Alt" descriptions for formulas and images.
116
+ //! - In case you have a link annotation that applies to text that wraps into one or multiple
117
+ //! new lines, you should use the `quad_points` functionality to indicate the exact covered
118
+ //! areas of the link.
119
+ //!
120
+ //! Once again, the above is only a best-effort summary, if you are interested in creating
121
+ //! completely well-tagged PDFs, you are advised to consult the given specifications.
122
+ //!
123
+ //! [`SerializeSettings`]: crate::SerializeSettings
124
+ //! [`Page`]: crate::page::Page
125
+ //! [`Surface`]: crate::surface::Surface
126
+ //! [`Document`]: crate::Document
127
+
128
+ use std::cell::LazyCell;
129
+ use std::cmp::PartialEq;
130
+ use std::collections::btree_map::Entry;
131
+ use std::collections::{BTreeMap, HashMap};
132
+ use std::io::Write as _;
133
+
134
+ use pdf_writer::types::{RoleMapOpts, StructRole, StructRole2};
135
+ use pdf_writer::writers::{PropertyList, StructElement};
136
+ use pdf_writer::{Chunk, Finish, Name, Ref, Str, TextStr};
137
+ use smallvec::SmallVec;
138
+
139
+ use crate::chunk_container::ChunkContainer;
140
+ use crate::configure::validate::VersionedFeature;
141
+ use crate::configure::{PdfVersion, ValidationError};
142
+ use crate::error::{KrillaError, KrillaResult};
143
+ use crate::geom::Rect;
144
+ use crate::page::page_root_transform;
145
+ use crate::serialize::SerializeContext;
146
+
147
+ pub use tag::*;
148
+
149
+ pub mod fmt;
150
+ mod tag;
151
+
152
+ /// An artifact that should not be part of the accessible structure.
153
+ #[derive(Copy, Clone, Debug, PartialEq, Default)]
154
+ pub struct Artifact {
155
+ /// The type of the artifact.
156
+ pub kind: ArtifactType,
157
+ /// The bounding box that incloses the artifact's visual content. Required
158
+ /// for background artifacts.
159
+ pub bbox: Option<Rect>,
160
+ }
161
+
162
+ impl Artifact {
163
+ /// Create a new artifact with a type and an optional BBox.
164
+ ///
165
+ /// You must provide a bounding box if `kind` is
166
+ /// [`ArtifactType::Background`] and you are targetting PDF 1.7, otherwise,
167
+ /// Krilla will later panic.
168
+ pub fn new(kind: ArtifactType, bbox: Option<Rect>) -> Self {
169
+ Self { kind, bbox }
170
+ }
171
+
172
+ /// Create a new artifact with a type and no bounding box.
173
+ pub fn with_kind(kind: ArtifactType) -> Self {
174
+ Self::new(kind, None)
175
+ }
176
+
177
+ /// Whether the artifacts requires a property list.
178
+ pub(crate) fn requires_properties(self, pdf_version: PdfVersion) -> bool {
179
+ self.bbox.is_some()
180
+ || self
181
+ .kind
182
+ .map_pdf_version(pdf_version)
183
+ .to_pdf_artifact_type()
184
+ .is_some()
185
+ }
186
+
187
+ /// Whether the artifact requires a BBox.
188
+ pub(crate) fn requires_bbox(self, pdf_version: PdfVersion) -> bool {
189
+ // Background artifacts were introduced in PDF 1.7. In table 330, the
190
+ // standard makes bounding boxes mandatory for background artifacts
191
+ // only. PDF 2.0 lifts this requirement in table 363.
192
+ //
193
+ // When changing the below condition, also change the error at the
194
+ // callsite.
195
+ self.kind == ArtifactType::Background && pdf_version == PdfVersion::Pdf17
196
+ }
197
+ }
198
+
199
+ /// A type of artifact.
200
+ #[derive(Copy, Clone, Debug, PartialEq, Default)]
201
+ pub enum ArtifactType {
202
+ /// The header of a page.
203
+ Header,
204
+ /// The footer of the page.
205
+ Footer,
206
+ /// For text in the back- or foreground of all pages.
207
+ Watermark,
208
+ /// Page numbers.
209
+ PageNumber,
210
+ /// Numbering artifacts before lines.
211
+ LineNumber,
212
+ /// Areas where there formerly was content, but which has been removed.
213
+ Redaction,
214
+ /// Bates numbering.
215
+ Bates,
216
+ /// Other artifacts arising from pagination not covered by the above variants.
217
+ PaginationOther,
218
+ /// Purely cosmetic typographical or design elements.
219
+ Layout,
220
+ /// Page artifacts, such as for example cut marks or color bars.
221
+ Page,
222
+ /// The background of a page or a graphical element.
223
+ Background,
224
+ /// Any other type of artifact.
225
+ #[default]
226
+ Other,
227
+ }
228
+
229
+ impl ArtifactType {
230
+ pub(crate) fn map_pdf_version(self, version: PdfVersion) -> Self {
231
+ match self {
232
+ Self::PageNumber | Self::LineNumber | Self::Redaction | Self::Bates
233
+ if version < PdfVersion::Pdf20 =>
234
+ {
235
+ ArtifactType::PaginationOther
236
+ }
237
+ Self::Header | Self::Footer | Self::Watermark if version < PdfVersion::Pdf17 => {
238
+ ArtifactType::PaginationOther
239
+ }
240
+ Self::Background if version < PdfVersion::Pdf17 => ArtifactType::Other,
241
+ _ => self,
242
+ }
243
+ }
244
+
245
+ pub(crate) fn to_pdf_artifact_type(self) -> Option<pdf_writer::types::ArtifactType> {
246
+ match self {
247
+ ArtifactType::Header => Some(pdf_writer::types::ArtifactType::Pagination),
248
+ ArtifactType::Footer => Some(pdf_writer::types::ArtifactType::Pagination),
249
+ ArtifactType::Watermark => Some(pdf_writer::types::ArtifactType::Pagination),
250
+ ArtifactType::PageNumber => Some(pdf_writer::types::ArtifactType::Pagination),
251
+ ArtifactType::LineNumber => Some(pdf_writer::types::ArtifactType::Pagination),
252
+ ArtifactType::Redaction => Some(pdf_writer::types::ArtifactType::Pagination),
253
+ ArtifactType::Bates => Some(pdf_writer::types::ArtifactType::Pagination),
254
+ ArtifactType::PaginationOther => Some(pdf_writer::types::ArtifactType::Pagination),
255
+ ArtifactType::Layout => Some(pdf_writer::types::ArtifactType::Layout),
256
+ ArtifactType::Page => Some(pdf_writer::types::ArtifactType::Page),
257
+ ArtifactType::Background => Some(pdf_writer::types::ArtifactType::Background),
258
+ ArtifactType::Other => None,
259
+ }
260
+ }
261
+
262
+ pub(crate) fn to_pdf_artifact_subtype(
263
+ self,
264
+ ) -> Option<pdf_writer::types::ArtifactSubtype<'static>> {
265
+ match self {
266
+ ArtifactType::Header => Some(pdf_writer::types::ArtifactSubtype::Header),
267
+ ArtifactType::Footer => Some(pdf_writer::types::ArtifactSubtype::Footer),
268
+ ArtifactType::Watermark => Some(pdf_writer::types::ArtifactSubtype::Watermark),
269
+ ArtifactType::PageNumber => Some(pdf_writer::types::ArtifactSubtype::PageNumber),
270
+ ArtifactType::LineNumber => Some(pdf_writer::types::ArtifactSubtype::LineNumber),
271
+ ArtifactType::Redaction => Some(pdf_writer::types::ArtifactSubtype::Redaction),
272
+ ArtifactType::Bates => Some(pdf_writer::types::ArtifactSubtype::Bates),
273
+ _ => None,
274
+ }
275
+ }
276
+ }
277
+
278
+ /// A language identifier as specified in RFC 3066. It will not be validated, so
279
+ /// it's on the user of the library to ensure the tag is valid.
280
+ pub type Lang<'a> = &'a str;
281
+ /// An alternate text that describes some element in natural language.
282
+ pub type Alt<'a> = &'a str;
283
+ /// The actual intended textual content of a span. For example,
284
+ /// if you have a hyphenated word, you can use `ActualText` to describe
285
+ /// the same word without hyphens.
286
+ pub type ActualText<'a> = &'a str;
287
+ /// The expanded form of an abbreviation.
288
+ pub type Expanded<'a> = &'a str;
289
+
290
+ /// A content tag associated with the content it wraps.
291
+ #[derive(Clone, Copy, Debug)]
292
+ pub enum ContentTag<'a> {
293
+ /// Artifacts represent pieces of content that are not really part of the logical structure
294
+ /// of a document and should be excluded in the logical tree. These include for example headers,
295
+ /// footers, page background and similar.
296
+ Artifact(Artifact),
297
+ /// A content tag that wraps some text with specific properties.
298
+ ///
299
+ /// Spans should not be too long. At most, they should contain a single line of text, but they
300
+ /// can obviously be shorter, if text within a single line contains text with different styles
301
+ /// or different languages.
302
+ Span(SpanTag<'a>),
303
+ /// Use this tag for anything else that does not semantically fit into `Span` or `Artifact`.
304
+ /// This includes for example arbitrary paths, images or a mix of different content that cannot
305
+ /// be split up more.
306
+ Other,
307
+ }
308
+
309
+ impl ContentTag<'_> {
310
+ pub(crate) fn name(&self) -> Name<'static> {
311
+ match self {
312
+ ContentTag::Artifact(_) => Name(b"Artifact"),
313
+ ContentTag::Span(_) => Name(b"Span"),
314
+ ContentTag::Other => Name(b"P"),
315
+ }
316
+ }
317
+
318
+ pub(crate) fn write_properties(
319
+ &self,
320
+ sc: &mut SerializeContext,
321
+ mut properties: PropertyList,
322
+ page_height: f32,
323
+ ) {
324
+ match self {
325
+ ContentTag::Artifact(artifact) => {
326
+ let at = artifact
327
+ .kind
328
+ .map_pdf_version(sc.serialize_settings().pdf_version());
329
+ let mut artifact_props = properties.artifact();
330
+
331
+ if let Some(bbox) = artifact.bbox {
332
+ let transform = page_root_transform(page_height);
333
+ let actual_rect = bbox.transform(transform).unwrap();
334
+ artifact_props.bounding_box(actual_rect.to_pdf_rect());
335
+ }
336
+
337
+ if sc.serialize_settings().pdf_version() >= PdfVersion::Pdf17 {
338
+ if at == ArtifactType::Header {
339
+ artifact_props.attached([pdf_writer::types::ArtifactAttachment::Top]);
340
+ }
341
+
342
+ if at == ArtifactType::Footer {
343
+ artifact_props.attached([pdf_writer::types::ArtifactAttachment::Bottom]);
344
+ }
345
+
346
+ if let Some(subtype) = at.to_pdf_artifact_subtype() {
347
+ artifact_props.subtype(subtype);
348
+ }
349
+ }
350
+
351
+ if let Some(artifact_type) = at.to_pdf_artifact_type() {
352
+ artifact_props.kind(artifact_type);
353
+ }
354
+ }
355
+ ContentTag::Span(SpanTag {
356
+ lang,
357
+ alt_text,
358
+ expanded,
359
+ actual_text,
360
+ }) => {
361
+ if let Some(lang) = lang {
362
+ properties.pair(Name(b"Lang"), TextStr(lang));
363
+ }
364
+
365
+ if let Some(alt) = alt_text {
366
+ if sc.serialize_settings().pdf_version() >= PdfVersion::Pdf15 {
367
+ properties.pair(Name(b"Alt"), TextStr(alt));
368
+ }
369
+ }
370
+
371
+ if let Some(exp) = expanded {
372
+ properties.pair(Name(b"E"), TextStr(exp));
373
+ }
374
+
375
+ if let Some(actual) = actual_text {
376
+ if sc.serialize_settings().pdf_version() >= PdfVersion::Pdf15 {
377
+ properties.actual_text(TextStr(actual));
378
+ }
379
+ }
380
+ }
381
+ ContentTag::Other => {}
382
+ }
383
+ }
384
+ }
385
+
386
+ /// A span tag.
387
+ #[derive(Clone, Copy, Debug)]
388
+ pub struct SpanTag<'a> {
389
+ /// The language of the text.
390
+ pub lang: Option<Lang<'a>>,
391
+ /// An optional alternate text that describes the text (for example, if the text consists
392
+ /// of a star symbol, the alt text should describe that in natural language).
393
+ pub alt_text: Option<Alt<'a>>,
394
+ /// If the content of the span is an abbreviation, the expanded form of the
395
+ /// abbreviation should be provided here.
396
+ pub expanded: Option<Expanded<'a>>,
397
+ /// The actual text represented by the glyphs, i.e. if you have a hyphenated span
398
+ /// `row-`, then you can wrap it in an `ActualText` to remove the hyphenation
399
+ /// when copy-pasting.
400
+ pub actual_text: Option<ActualText<'a>>,
401
+ }
402
+
403
+ impl<'a> SpanTag<'a> {
404
+ /// An empty span tag.
405
+ pub fn empty() -> Self {
406
+ Self {
407
+ lang: None,
408
+ alt_text: None,
409
+ expanded: None,
410
+ actual_text: None,
411
+ }
412
+ }
413
+
414
+ /// Sets [`SpanTag::lang`].
415
+ pub fn with_lang(mut self, lang: Option<&'a str>) -> Self {
416
+ self.lang = lang;
417
+ self
418
+ }
419
+
420
+ /// Sets [`SpanTag::alt_text`].
421
+ pub fn with_alt_text(mut self, alt_text: Option<&'a str>) -> Self {
422
+ self.alt_text = alt_text;
423
+ self
424
+ }
425
+
426
+ /// Sets [`SpanTag::expanded`].
427
+ pub fn with_expanded(mut self, expanded: Option<&'a str>) -> Self {
428
+ self.expanded = expanded;
429
+ self
430
+ }
431
+
432
+ /// Sets [`SpanTag::actual_text`].
433
+ pub fn with_actual_text(mut self, actual_text: Option<&'a str>) -> Self {
434
+ self.actual_text = actual_text;
435
+ self
436
+ }
437
+ }
438
+
439
+ #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
440
+ pub(crate) struct PageTagIdentifier {
441
+ pub(crate) page_index: usize,
442
+ pub(crate) mcid: i32,
443
+ }
444
+
445
+ impl From<PageTagIdentifier> for IdentifierType {
446
+ fn from(value: PageTagIdentifier) -> Self {
447
+ IdentifierType::PageIdentifier(value)
448
+ }
449
+ }
450
+
451
+ impl From<PageTagIdentifier> for Identifier {
452
+ fn from(value: PageTagIdentifier) -> Self {
453
+ Identifier(IdentifierInner::Real(value.into()))
454
+ }
455
+ }
456
+
457
+ impl PageTagIdentifier {
458
+ pub(crate) fn new(page_index: usize, mcid: i32) -> Self {
459
+ Self { page_index, mcid }
460
+ }
461
+
462
+ pub(crate) fn bump(&mut self) -> PageTagIdentifier {
463
+ let old = *self;
464
+
465
+ self.mcid = self.mcid.checked_add(1).unwrap();
466
+
467
+ old
468
+ }
469
+ }
470
+
471
+ #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
472
+ pub(crate) struct AnnotationIdentifier {
473
+ pub(crate) page_index: usize,
474
+ pub(crate) annot_index: usize,
475
+ }
476
+
477
+ impl From<AnnotationIdentifier> for IdentifierType {
478
+ fn from(value: AnnotationIdentifier) -> Self {
479
+ IdentifierType::AnnotationIdentifier(value)
480
+ }
481
+ }
482
+
483
+ impl From<AnnotationIdentifier> for Identifier {
484
+ fn from(value: AnnotationIdentifier) -> Self {
485
+ Identifier(IdentifierInner::Real(value.into()))
486
+ }
487
+ }
488
+
489
+ impl AnnotationIdentifier {
490
+ pub fn new(page_index: usize, annot_index: usize) -> Self {
491
+ Self {
492
+ page_index,
493
+ annot_index,
494
+ }
495
+ }
496
+ }
497
+
498
+ #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
499
+ pub(crate) enum IdentifierType {
500
+ PageIdentifier(PageTagIdentifier),
501
+ AnnotationIdentifier(AnnotationIdentifier),
502
+ }
503
+
504
+ #[derive(Copy, Clone, Debug, Eq, PartialEq)]
505
+ pub(crate) enum IdentifierInner {
506
+ Real(IdentifierType),
507
+ Dummy,
508
+ }
509
+
510
+ /// An identifier for an annotation or certain parts of page content.
511
+ ///
512
+ /// Need to be used as a leaf node in a tag tree.
513
+ #[derive(Copy, Clone, Debug, Eq, PartialEq)]
514
+ pub struct Identifier(pub(crate) IdentifierInner);
515
+
516
+ impl Identifier {
517
+ pub(crate) fn new_annotation(page_index: usize, annot_index: usize) -> Self {
518
+ AnnotationIdentifier::new(page_index, annot_index).into()
519
+ }
520
+
521
+ pub(crate) fn dummy() -> Self {
522
+ Self(IdentifierInner::Dummy)
523
+ }
524
+ }
525
+
526
+ impl TagKind {
527
+ pub(crate) fn write_kind(&self, struct_elem: &mut StructElement, sc: &mut SerializeContext) {
528
+ let pdf_version = sc.serialize_settings().pdf_version();
529
+ if pdf_version < self.minimum_version() {
530
+ // Fall back to P in case the tag is not supported with the current
531
+ // PDF version
532
+ struct_elem.kind(StructRole::P);
533
+ return;
534
+ }
535
+
536
+ match self {
537
+ Self::Part(_) => write_kind_compat(sc, struct_elem, StructRole2::Part),
538
+ Self::Article(_) => write_kind_1_7(struct_elem, StructRole::Art),
539
+ Self::Section(_) => write_kind_compat(sc, struct_elem, StructRole2::Sect),
540
+ Self::Div(_) => write_kind_compat(sc, struct_elem, StructRole2::Div),
541
+ Self::BlockQuote(_) => write_kind_1_7(struct_elem, StructRole::BlockQuote),
542
+ Self::Caption(_) => write_kind_compat(sc, struct_elem, StructRole2::Caption),
543
+ Self::TOC(_) => write_kind_1_7(struct_elem, StructRole::TOC),
544
+ Self::TOCI(_) => write_kind_1_7(struct_elem, StructRole::TOCI),
545
+ Self::Index(_) => write_kind_1_7(struct_elem, StructRole::Index),
546
+ Self::P(_) => write_kind_compat(sc, struct_elem, StructRole2::P),
547
+ Self::L(_) => write_kind_compat(sc, struct_elem, StructRole2::L),
548
+ Self::LI(_) => write_kind_compat(sc, struct_elem, StructRole2::LI),
549
+ Self::Lbl(_) => write_kind_compat(sc, struct_elem, StructRole2::Lbl),
550
+ Self::LBody(_) => write_kind_compat(sc, struct_elem, StructRole2::LBody),
551
+ Self::Table(_) => write_kind_compat(sc, struct_elem, StructRole2::Table),
552
+ Self::TR(_) => write_kind_compat(sc, struct_elem, StructRole2::TR),
553
+ Self::TH(_) => write_kind_compat(sc, struct_elem, StructRole2::TH),
554
+ Self::TD(_) => write_kind_compat(sc, struct_elem, StructRole2::TD),
555
+ Self::THead(_) => write_kind_compat(sc, struct_elem, StructRole2::THead),
556
+ Self::TBody(_) => write_kind_compat(sc, struct_elem, StructRole2::TBody),
557
+ Self::TFoot(_) => write_kind_compat(sc, struct_elem, StructRole2::TFoot),
558
+ Self::Span(_) => write_kind_compat(sc, struct_elem, StructRole2::Span),
559
+ Self::InlineQuote(_) => write_kind_1_7(struct_elem, StructRole::Quote),
560
+ Self::Note(_) => write_kind_1_7(struct_elem, StructRole::Note),
561
+ Self::Reference(_) => write_kind_1_7(struct_elem, StructRole::Reference),
562
+ Self::BibEntry(_) => write_kind_1_7(struct_elem, StructRole::BibEntry),
563
+ Self::Code(_) => write_kind_1_7(struct_elem, StructRole::Code),
564
+ Self::Link(_) => write_kind_compat(sc, struct_elem, StructRole2::Link),
565
+ Self::Annot(_) => write_kind_compat(sc, struct_elem, StructRole2::Annot),
566
+ Self::Figure(_) => write_kind_compat(sc, struct_elem, StructRole2::Figure),
567
+ Self::Formula(_) => write_kind_compat(sc, struct_elem, StructRole2::Formula),
568
+ Self::Form(_) => write_kind_compat(sc, struct_elem, StructRole2::Form),
569
+ Self::NonStruct(_) => write_kind_compat(sc, struct_elem, StructRole2::NonStruct),
570
+ // Custom structure roles that are registered in the `RoleMap`.
571
+ Self::Datetime(_) => write_kind_custom(sc, struct_elem, Name(b"Datetime")),
572
+ Self::Terms(_) => write_kind_custom(sc, struct_elem, Name(b"Terms")),
573
+ Self::Title(_) => write_kind_custom(sc, struct_elem, Name(b"Title")),
574
+ // PDF 2.0 structure roles that are conditionally registered.
575
+ Self::Hn(tag) => {
576
+ let role2 = StructRole2::Heading(tag.level());
577
+ if pdf_version < PdfVersion::Pdf20 {
578
+ // Dynamically register custom headings `Hn` if the level
579
+ // (`n >= 7`) isn't supported by PDF 1.7 and below.
580
+ let compat = role2.compatibility_1_7(RoleMapOpts::default());
581
+ if compat.into_pdf_1_7().is_none() {
582
+ sc.global_objects.custom_heading_roles.insert(tag.level());
583
+ }
584
+ struct_elem.custom_kind(role2.to_name(&mut [0; 6]));
585
+ } else {
586
+ struct_elem.kind_2(role2, sc.pdf2_ns.ssn_ref);
587
+ }
588
+ }
589
+ Self::Strong(_) => {
590
+ if pdf_version < PdfVersion::Pdf20 {
591
+ struct_elem.custom_kind(Name(b"Strong"));
592
+ } else {
593
+ struct_elem.kind_2(StructRole2::Strong, sc.pdf2_ns.ssn_ref);
594
+ }
595
+ }
596
+ Self::Em(_) => {
597
+ if pdf_version < PdfVersion::Pdf20 {
598
+ struct_elem.custom_kind(Name(b"Em"));
599
+ } else {
600
+ struct_elem.kind_2(StructRole2::Em, sc.pdf2_ns.ssn_ref);
601
+ }
602
+ }
603
+ };
604
+ }
605
+
606
+ pub(crate) fn minimum_version(&self) -> PdfVersion {
607
+ match self {
608
+ Self::Part(_) => PdfVersion::Pdf14,
609
+ Self::Article(_) => PdfVersion::Pdf14,
610
+ Self::Section(_) => PdfVersion::Pdf14,
611
+ Self::Div(_) => PdfVersion::Pdf14,
612
+ Self::BlockQuote(_) => PdfVersion::Pdf14,
613
+ Self::Caption(_) => PdfVersion::Pdf14,
614
+ Self::TOC(_) => PdfVersion::Pdf14,
615
+ Self::TOCI(_) => PdfVersion::Pdf14,
616
+ Self::Index(_) => PdfVersion::Pdf14,
617
+ Self::P(_) => PdfVersion::Pdf14,
618
+ Self::Hn(_) => PdfVersion::Pdf14,
619
+ Self::L(_) => PdfVersion::Pdf14,
620
+ Self::LI(_) => PdfVersion::Pdf14,
621
+ Self::Lbl(_) => PdfVersion::Pdf14,
622
+ Self::LBody(_) => PdfVersion::Pdf14,
623
+ Self::Table(_) => PdfVersion::Pdf14,
624
+ Self::TR(_) => PdfVersion::Pdf14,
625
+ Self::TH(_) => PdfVersion::Pdf14,
626
+ Self::TD(_) => PdfVersion::Pdf14,
627
+ // TODO: writing `P` tags in PDF 1.4 will break the table structure.
628
+ // Instead consider just transparently inserting all children, which
629
+ // should be `TR`s anyway.
630
+ Self::THead(_) => PdfVersion::Pdf15,
631
+ Self::TBody(_) => PdfVersion::Pdf15,
632
+ Self::TFoot(_) => PdfVersion::Pdf15,
633
+ Self::Span(_) => PdfVersion::Pdf14,
634
+ Self::InlineQuote(_) => PdfVersion::Pdf14,
635
+ Self::Note(_) => PdfVersion::Pdf14,
636
+ Self::Reference(_) => PdfVersion::Pdf14,
637
+ Self::BibEntry(_) => PdfVersion::Pdf14,
638
+ Self::Code(_) => PdfVersion::Pdf14,
639
+ Self::Link(_) => PdfVersion::Pdf14,
640
+ Self::Annot(_) => PdfVersion::Pdf15,
641
+ Self::Figure(_) => PdfVersion::Pdf14,
642
+ Self::Formula(_) => PdfVersion::Pdf14,
643
+ Self::Form(_) => PdfVersion::Pdf14,
644
+ Self::NonStruct(_) => PdfVersion::Pdf14,
645
+ Self::Datetime(_) => PdfVersion::Pdf14,
646
+ Self::Terms(_) => PdfVersion::Pdf14,
647
+ Self::Title(_) => PdfVersion::Pdf14,
648
+ Self::Strong(_) => PdfVersion::Pdf14,
649
+ Self::Em(_) => PdfVersion::Pdf14,
650
+ }
651
+ }
652
+
653
+ pub(crate) fn should_have_alt(&self) -> bool {
654
+ matches!(self, TagKind::Figure(_) | TagKind::Formula(_))
655
+ }
656
+
657
+ pub(crate) fn can_have_title(&self) -> bool {
658
+ matches!(self, Self::Hn(_))
659
+ }
660
+ }
661
+
662
+ fn write_kind_1_7(struct_elem: &mut StructElement, role: StructRole) {
663
+ struct_elem.kind(role);
664
+ }
665
+
666
+ /// If serializing a PDF 2.0 document, write a PDF 2.0 structure role, otherwise
667
+ /// fall back to the compatible PDF 1.7 role.
668
+ fn write_kind_compat(
669
+ sc: &mut SerializeContext,
670
+ struct_elem: &mut StructElement,
671
+ role: StructRole2,
672
+ ) {
673
+ if sc.serialize_settings().pdf_version() < PdfVersion::Pdf20 {
674
+ let compat = role.compatibility_1_7(RoleMapOpts::default());
675
+ struct_elem.kind(compat.role());
676
+ } else {
677
+ struct_elem.kind_2(role, sc.pdf2_ns.ssn_ref);
678
+ }
679
+ }
680
+
681
+ /// Write a custom role-mapped structure role. If serializing a PDF 2.0 document
682
+ /// also write the custom krilla namespace.
683
+ fn write_kind_custom(sc: &mut SerializeContext, struct_elem: &mut StructElement, name: Name) {
684
+ struct_elem.custom_kind(name);
685
+ if sc.serialize_settings().pdf_version() >= PdfVersion::Pdf20 {
686
+ struct_elem.namespace(sc.pdf2_ns.krilla_ref);
687
+ }
688
+ }
689
+
690
+ /// A node in a tag tree.
691
+ #[derive(Debug, Clone, PartialEq)]
692
+ pub enum Node {
693
+ /// A group node.
694
+ Group(TagGroup),
695
+ /// A leaf node.
696
+ Leaf(Identifier),
697
+ }
698
+
699
+ impl Node {
700
+ pub(crate) fn serialize(
701
+ &self,
702
+ sc: &mut SerializeContext,
703
+ parent_tree_map: &mut HashMap<IdentifierType, Ref>,
704
+ id_tree: &mut BTreeMap<TagId, Ref>,
705
+ parent: Ref,
706
+ note_id: &mut u32,
707
+ struct_elems: &mut Chunk,
708
+ ) -> KrillaResult<Option<Reference>> {
709
+ match self {
710
+ Node::Group(g) => Ok(Some(g.serialize(
711
+ sc,
712
+ parent_tree_map,
713
+ id_tree,
714
+ parent,
715
+ note_id,
716
+ struct_elems,
717
+ )?)),
718
+ Node::Leaf(ci) => match ci.0 {
719
+ IdentifierInner::Real(rci) => Ok(Some(Reference::ContentIdentifier(rci))),
720
+ IdentifierInner::Dummy => Ok(None),
721
+ },
722
+ }
723
+ }
724
+ }
725
+
726
+ impl From<TagGroup> for Node {
727
+ fn from(value: TagGroup) -> Self {
728
+ Node::Group(value)
729
+ }
730
+ }
731
+
732
+ impl From<Identifier> for Node {
733
+ fn from(value: Identifier) -> Self {
734
+ Node::Leaf(value)
735
+ }
736
+ }
737
+
738
+ #[derive(Clone, Copy)]
739
+ pub(crate) enum Reference {
740
+ Ref(Ref),
741
+ ContentIdentifier(IdentifierType),
742
+ }
743
+
744
+ /// A tag group.
745
+ #[derive(Debug, Clone, PartialEq)]
746
+ pub struct TagGroup {
747
+ /// The tag of the tag group.
748
+ pub tag: TagKind,
749
+ /// The children of the tag group.
750
+ pub children: Vec<Node>,
751
+ }
752
+
753
+ impl TagGroup {
754
+ /// Create a new tag group with a specific tag.
755
+ pub fn new(tag: impl Into<TagKind>) -> Self {
756
+ Self {
757
+ tag: tag.into(),
758
+ children: vec![],
759
+ }
760
+ }
761
+
762
+ /// Create a new tag group with a specific tag and a list of children.
763
+ pub fn with_children(tag: impl Into<TagKind>, children: Vec<Node>) -> Self {
764
+ Self {
765
+ tag: tag.into(),
766
+ children,
767
+ }
768
+ }
769
+
770
+ /// Append a new child to the tag group.
771
+ pub fn push(&mut self, child: impl Into<Node>) {
772
+ self.children.push(child.into())
773
+ }
774
+
775
+ pub(crate) fn serialize(
776
+ &self,
777
+ sc: &mut SerializeContext,
778
+ parent_tree_map: &mut HashMap<IdentifierType, Ref>,
779
+ id_tree: &mut BTreeMap<TagId, Ref>,
780
+ parent_ref: Ref,
781
+ note_id: &mut u32,
782
+ struct_elems: &mut Chunk,
783
+ ) -> KrillaResult<Reference> {
784
+ let elem_ref = sc.new_ref();
785
+ let mut children_refs = vec![];
786
+
787
+ for child in &self.children {
788
+ let serialized = child.serialize(
789
+ sc,
790
+ parent_tree_map,
791
+ id_tree,
792
+ elem_ref,
793
+ note_id,
794
+ struct_elems,
795
+ )?;
796
+ if let Some(ref_) = serialized {
797
+ children_refs.push(ref_);
798
+ }
799
+ }
800
+
801
+ let mut struct_elem = struct_elems.struct_element(elem_ref);
802
+ self.tag.write_kind(&mut struct_elem, sc);
803
+ struct_elem.parent(parent_ref);
804
+
805
+ let tag = self.tag.as_any();
806
+ let pdf_version = sc.serialize_settings().pdf_version();
807
+
808
+ if let Some(id) = tag.id() {
809
+ match id_tree.entry(id.clone()) {
810
+ Entry::Vacant(vacant) => {
811
+ struct_elem.id(Str(id.as_bytes()));
812
+ vacant.insert(elem_ref);
813
+ }
814
+ Entry::Occupied(_) => {
815
+ return Err(KrillaError::DuplicateTagId(id.clone(), tag.location));
816
+ }
817
+ }
818
+ } else if matches!(self.tag, TagKind::Note(_)) {
819
+ // Explicitly don't use `TagId::from_bytes` to disambiguate note IDs
820
+ // from user provided IDs.
821
+ let mut id = TagId(SmallVec::new());
822
+ _ = write!(&mut id.0, "Note {note_id}");
823
+ struct_elem.id(Str(id.as_bytes()));
824
+ id_tree.insert(id, elem_ref);
825
+
826
+ *note_id += 1;
827
+ }
828
+
829
+ if self.tag.can_have_title() && tag.title().is_none_or(str::is_empty) {
830
+ sc.register_validation_error(ValidationError::MissingHeadingTitle);
831
+ }
832
+
833
+ if self.tag.should_have_alt() && tag.alt_text().is_none_or(str::is_empty) {
834
+ sc.register_validation_error(ValidationError::MissingAltText(tag.location));
835
+ }
836
+
837
+ // TODO: Once we have a generalized mechanism for validating tag trees,
838
+ // validate TH-outside-THead structure for PDF < 1.5.
839
+
840
+ for attr in tag.attrs.iter() {
841
+ let Attr::Struct(attr) = attr else {
842
+ continue;
843
+ };
844
+ match attr {
845
+ StructAttr::Id(_) => (), // Handled above
846
+ StructAttr::Title(title) => {
847
+ struct_elem.title(TextStr(title));
848
+ }
849
+ StructAttr::Lang(lang) => {
850
+ if pdf_version >= PdfVersion::Pdf14 {
851
+ struct_elem.lang(TextStr(lang));
852
+ }
853
+ }
854
+ StructAttr::AltText(alt) => {
855
+ struct_elem.alt(TextStr(alt));
856
+ }
857
+ StructAttr::Expanded(expanded) => {
858
+ if pdf_version >= PdfVersion::Pdf15 {
859
+ struct_elem.expanded(TextStr(expanded));
860
+ }
861
+ }
862
+ StructAttr::ActualText(actual_text) => {
863
+ if pdf_version >= PdfVersion::Pdf14 {
864
+ struct_elem.actual_text(TextStr(actual_text));
865
+ }
866
+ }
867
+
868
+ // Not really an attribute
869
+ StructAttr::HeadingLevel(_) => (),
870
+ }
871
+ }
872
+
873
+ let mut attributes = LazyCell::new(|| struct_elem.attributes());
874
+
875
+ // Lazily initialize the list attributes to avoid an empty array.
876
+ let mut list_attributes = LazyCell::new(|| attributes.push().list());
877
+ for attr in tag.attrs.iter() {
878
+ let Attr::List(attr) = attr else {
879
+ continue;
880
+ };
881
+ match attr {
882
+ ListAttr::Numbering(numbering) => {
883
+ list_attributes.list_numbering(numbering.to_pdf());
884
+ }
885
+ }
886
+ }
887
+ list_attributes.finish();
888
+
889
+ // Lazily initialize the table attributes to avoid an empty array.
890
+ let mut table_attributes = LazyCell::new(|| attributes.push().table());
891
+ for attr in tag.attrs.iter() {
892
+ let Attr::Table(attr) = attr else {
893
+ continue;
894
+ };
895
+ match attr {
896
+ TableAttr::Summary(summary) => {
897
+ if pdf_version >= PdfVersion::Pdf17 {
898
+ table_attributes.summary(TextStr(summary));
899
+ }
900
+ }
901
+ TableAttr::HeaderScope(scope) => {
902
+ if pdf_version >= VersionedFeature::TableHeaderScope.minimum_pdf_version() {
903
+ table_attributes.scope(scope.to_pdf());
904
+ } else if scope == &TableHeaderScope::Row {
905
+ // Without `Scope`, the correct cell to point to is ambiguous.
906
+ sc.register_validation_error(ValidationError::RequiresNewerPdfVersion(
907
+ VersionedFeature::TableHeaderScope,
908
+ tag.location,
909
+ ));
910
+ }
911
+ }
912
+ TableAttr::CellHeaders(headers) => {
913
+ if pdf_version >= PdfVersion::Pdf15 {
914
+ let id_strs = headers.iter().map(|id| Str(id.as_bytes()));
915
+ table_attributes.headers().items(id_strs);
916
+ }
917
+ }
918
+ TableAttr::RowSpan(n) => {
919
+ table_attributes.row_span(n.get() as i32);
920
+ }
921
+ TableAttr::ColSpan(n) => {
922
+ table_attributes.col_span(n.get() as i32);
923
+ }
924
+ }
925
+ }
926
+ table_attributes.finish();
927
+
928
+ // Lazily initialize the list attributes to avoid an empty array.
929
+ let mut layout_attributes = LazyCell::new(|| attributes.push().layout());
930
+ for attr in tag.attrs.iter() {
931
+ let Attr::Layout(attr) = attr else {
932
+ continue;
933
+ };
934
+ match attr {
935
+ LayoutAttr::Placement(placement) => {
936
+ layout_attributes.placement(placement.to_pdf());
937
+ }
938
+ LayoutAttr::WritingMode(writing_mode) => {
939
+ layout_attributes.writing_mode(writing_mode.to_pdf());
940
+ }
941
+ &LayoutAttr::BBox(BBox { page_idx, rect }) => {
942
+ let Some(page_info) = sc.page_infos().get(page_idx) else {
943
+ panic!(
944
+ "tag tree contains bounding box with page index {page_idx}, \
945
+ but document only has {} pages",
946
+ sc.page_infos().len()
947
+ );
948
+ };
949
+ let transform = page_root_transform(page_info.size().height());
950
+ let actual_rect = rect.transform(transform).unwrap();
951
+ layout_attributes.bbox(actual_rect.to_pdf_rect());
952
+ }
953
+ &LayoutAttr::Width(width) => {
954
+ layout_attributes.width(width);
955
+ }
956
+ &LayoutAttr::Height(height) => {
957
+ layout_attributes.height(height);
958
+ }
959
+ &LayoutAttr::BackgroundColor(color) => {
960
+ if pdf_version >= PdfVersion::Pdf15 {
961
+ layout_attributes.background_color(color.into());
962
+ }
963
+ }
964
+ LayoutAttr::BorderColor(sides) => {
965
+ if pdf_version >= PdfVersion::Pdf15 {
966
+ let sides = sides.map_pdf(NaiveRgbColor::into_f32_array);
967
+ layout_attributes.border_color(sides);
968
+ }
969
+ }
970
+ LayoutAttr::BorderStyle(sides) => {
971
+ if pdf_version >= PdfVersion::Pdf15 {
972
+ let sides = sides.map_pdf(BorderStyle::to_pdf);
973
+ layout_attributes.border_style(sides);
974
+ }
975
+ }
976
+ LayoutAttr::BorderThickness(sides) => {
977
+ if pdf_version >= PdfVersion::Pdf15 {
978
+ layout_attributes.border_thickness(sides.into_pdf());
979
+ }
980
+ }
981
+ LayoutAttr::Padding(sides) => {
982
+ if pdf_version >= PdfVersion::Pdf15 {
983
+ layout_attributes.padding(sides.into_pdf());
984
+ }
985
+ }
986
+ &LayoutAttr::Color(color) => {
987
+ if pdf_version >= PdfVersion::Pdf15 {
988
+ layout_attributes.color(color.into());
989
+ }
990
+ }
991
+ &LayoutAttr::SpaceBefore(margin) => {
992
+ layout_attributes.space_before(margin);
993
+ }
994
+ &LayoutAttr::SpaceAfter(margin) => {
995
+ layout_attributes.space_after(margin);
996
+ }
997
+ &LayoutAttr::StartIndent(margin) => {
998
+ layout_attributes.start_indent(margin);
999
+ }
1000
+ &LayoutAttr::EndIndent(margin) => {
1001
+ layout_attributes.end_indent(margin);
1002
+ }
1003
+ &LayoutAttr::TextIndent(indent) => {
1004
+ layout_attributes.text_indent(indent);
1005
+ }
1006
+ LayoutAttr::BlockAlign(alignment) => {
1007
+ layout_attributes.block_align(alignment.to_pdf());
1008
+ }
1009
+ LayoutAttr::InlineAlign(alignment) => {
1010
+ layout_attributes.inline_align(alignment.to_pdf());
1011
+ }
1012
+ LayoutAttr::TextAlign(alignment) => {
1013
+ layout_attributes.text_align(alignment.to_pdf());
1014
+ }
1015
+ LayoutAttr::TableBorderStyle(sides) => {
1016
+ if pdf_version >= PdfVersion::Pdf15 {
1017
+ let sides = sides.map_pdf(BorderStyle::to_pdf);
1018
+ layout_attributes.table_border_style(sides);
1019
+ }
1020
+ }
1021
+ LayoutAttr::TablePadding(sides) => {
1022
+ if pdf_version >= PdfVersion::Pdf15 {
1023
+ layout_attributes.table_padding(sides.into_pdf());
1024
+ }
1025
+ }
1026
+ &LayoutAttr::BaselineShift(shift) => {
1027
+ layout_attributes.baseline_shift(shift);
1028
+ }
1029
+ LayoutAttr::LineHeight(height) => {
1030
+ layout_attributes.line_height(height.to_pdf());
1031
+ }
1032
+ &LayoutAttr::TextDecorationColor(color) => {
1033
+ if pdf_version >= PdfVersion::Pdf15 {
1034
+ layout_attributes.text_decoration_color(color.into());
1035
+ }
1036
+ }
1037
+ &LayoutAttr::TextDecorationThickness(thickness) => {
1038
+ if pdf_version >= PdfVersion::Pdf15 {
1039
+ layout_attributes.text_decoration_thickness(thickness);
1040
+ }
1041
+ }
1042
+ LayoutAttr::TextDecorationType(style) => {
1043
+ if pdf_version >= PdfVersion::Pdf15 {
1044
+ layout_attributes.text_decoration_type(style.to_pdf());
1045
+ }
1046
+ }
1047
+ &LayoutAttr::GlyphOrientationVertical(orientation) => {
1048
+ if pdf_version >= PdfVersion::Pdf15 {
1049
+ layout_attributes.glyph_orientation_vertical(orientation.to_pdf());
1050
+ }
1051
+ }
1052
+ LayoutAttr::ColumnCount(columns) => {
1053
+ if pdf_version >= PdfVersion::Pdf16 {
1054
+ layout_attributes.column_count(columns.get() as i32);
1055
+ }
1056
+ }
1057
+ LayoutAttr::ColumnGap(gap) => {
1058
+ if pdf_version >= PdfVersion::Pdf16 {
1059
+ let sizes = layout_attributes.column_gap();
1060
+ match gap {
1061
+ ColumnDimensions::All(gap) => sizes.uniform(*gap),
1062
+ ColumnDimensions::Specific(values) => {
1063
+ sizes.individual().items(values.iter().copied());
1064
+ }
1065
+ }
1066
+ }
1067
+ }
1068
+ LayoutAttr::ColumnWidths(width) => {
1069
+ if pdf_version >= PdfVersion::Pdf16 {
1070
+ let sizes = layout_attributes.column_widths();
1071
+ match width {
1072
+ ColumnDimensions::All(width) => sizes.uniform(*width),
1073
+ ColumnDimensions::Specific(values) => {
1074
+ sizes.individual().items(values.iter().copied());
1075
+ }
1076
+ }
1077
+ }
1078
+ }
1079
+ }
1080
+ }
1081
+ layout_attributes.finish();
1082
+
1083
+ attributes.finish();
1084
+
1085
+ serialize_children(
1086
+ sc,
1087
+ elem_ref,
1088
+ children_refs,
1089
+ parent_tree_map,
1090
+ &mut struct_elem,
1091
+ )?;
1092
+ struct_elem.finish();
1093
+
1094
+ Ok(Reference::Ref(elem_ref))
1095
+ }
1096
+
1097
+ fn validate(&self, id_tree: &BTreeMap<TagId, Ref>) -> KrillaResult<()> {
1098
+ if let Some(headers) = self.tag.headers() {
1099
+ for id in headers.iter() {
1100
+ if !id_tree.contains_key(id) {
1101
+ return Err(KrillaError::UnknownTagId(id.clone(), self.tag.location()));
1102
+ }
1103
+ }
1104
+ }
1105
+
1106
+ for child in self.children.iter() {
1107
+ if let Node::Group(group) = child {
1108
+ group.validate(id_tree)?;
1109
+ }
1110
+ }
1111
+ Ok(())
1112
+ }
1113
+ }
1114
+
1115
+ /// A tag tree.
1116
+ #[derive(Default)]
1117
+ pub struct TagTree {
1118
+ /// The children of the tag tree.
1119
+ pub children: Vec<Node>,
1120
+ /// Language attribute for the auto-generated Document root structure element.
1121
+ pub lang: Option<String>,
1122
+ }
1123
+
1124
+ impl From<Vec<Node>> for TagTree {
1125
+ fn from(children: Vec<Node>) -> Self {
1126
+ Self {
1127
+ children,
1128
+ lang: None,
1129
+ }
1130
+ }
1131
+ }
1132
+
1133
+ impl TagTree {
1134
+ /// Create a new tag tree.
1135
+ pub fn new() -> Self {
1136
+ Self {
1137
+ children: vec![],
1138
+ lang: None,
1139
+ }
1140
+ }
1141
+
1142
+ /// Set the language attribute on the Document root structure element.
1143
+ ///
1144
+ /// This sets `/Lang` on the auto-generated Document structure element,
1145
+ /// which is distinct from the catalog-level `/Lang` set via
1146
+ /// [`Metadata::language`](crate::metadata::Metadata::language).
1147
+ pub fn with_lang(mut self, lang: Option<String>) -> Self {
1148
+ self.lang = lang;
1149
+ self
1150
+ }
1151
+
1152
+ /// Append a new child to the tag tree.
1153
+ pub fn push(&mut self, child: impl Into<Node>) {
1154
+ self.children.push(child.into())
1155
+ }
1156
+
1157
+ pub(crate) fn serialize(
1158
+ &self,
1159
+ sc: &mut SerializeContext,
1160
+ chunk_container: &mut ChunkContainer,
1161
+ parent_tree_map: &mut HashMap<IdentifierType, Ref>,
1162
+ id_tree_map: &mut BTreeMap<TagId, Ref>,
1163
+ struct_tree_ref: Ref,
1164
+ ) -> KrillaResult<Ref> {
1165
+ let root_ref = sc.new_ref();
1166
+ let mut struct_elems = sc.new_chunk();
1167
+
1168
+ // Keeps track of the ID of notes in the IDTree. We currently only write IDs for notes,
1169
+ // which is why we use this simple variable, but this should be refactored if we write
1170
+ // the IDs for multiple types of struct elements in the future.
1171
+ let mut note_id = 1;
1172
+
1173
+ let mut children_refs = vec![];
1174
+
1175
+ for child in &self.children {
1176
+ let serialized = child.serialize(
1177
+ sc,
1178
+ parent_tree_map,
1179
+ id_tree_map,
1180
+ root_ref,
1181
+ &mut note_id,
1182
+ &mut struct_elems,
1183
+ )?;
1184
+
1185
+ if let Some(ref_) = serialized {
1186
+ children_refs.push(ref_);
1187
+ }
1188
+ }
1189
+
1190
+ let mut struct_elem = struct_elems.indirect(root_ref).start::<StructElement>();
1191
+ struct_elem.kind(StructRole::Document);
1192
+ struct_elem.parent(struct_tree_ref);
1193
+ if let Some(lang) = &self.lang {
1194
+ if sc.serialize_settings().pdf_version() >= PdfVersion::Pdf14 {
1195
+ struct_elem.lang(TextStr(lang));
1196
+ }
1197
+ }
1198
+ serialize_children(
1199
+ sc,
1200
+ root_ref,
1201
+ children_refs,
1202
+ parent_tree_map,
1203
+ &mut struct_elem,
1204
+ )?;
1205
+
1206
+ struct_elem.finish();
1207
+ chunk_container.non_stream.struct_elements = Some(struct_elems);
1208
+
1209
+ Ok(root_ref)
1210
+ }
1211
+
1212
+ pub(crate) fn validate(&self, id_tree: &BTreeMap<TagId, Ref>) -> KrillaResult<()> {
1213
+ for child in self.children.iter() {
1214
+ if let Node::Group(group) = child {
1215
+ group.validate(id_tree)?;
1216
+ }
1217
+ }
1218
+ Ok(())
1219
+ }
1220
+ }
1221
+
1222
+ fn serialize_children(
1223
+ sc: &mut SerializeContext,
1224
+ parent_ref: Ref,
1225
+ children_refs: Vec<Reference>,
1226
+ parent_tree_map: &mut HashMap<IdentifierType, Ref>,
1227
+ struct_elem: &mut StructElement,
1228
+ ) -> KrillaResult<()> {
1229
+ // We can define a /Pg element on the struct element. If a marked content reference
1230
+ // is part of the same page as that entry, we can just write the mcid, otherwise, we
1231
+ // need to write a full marked content reference.
1232
+ // In our case, we just use the first marked content reference we can find as the
1233
+ // entry in the /Pg dict.
1234
+ let mut struct_page_ref = None;
1235
+ let mut struct_children = struct_elem.children();
1236
+
1237
+ for child in children_refs {
1238
+ match child {
1239
+ Reference::Ref(r) => {
1240
+ struct_children.struct_element(r);
1241
+ }
1242
+ Reference::ContentIdentifier(it) => match it {
1243
+ IdentifierType::PageIdentifier(pi) => {
1244
+ let page_ref = sc
1245
+ .page_infos()
1246
+ .get(pi.page_index)
1247
+ .unwrap_or_else(|| panic!("tag tree contains identifier from page {}, but document only has {} pages",
1248
+ pi.page_index + 1,
1249
+ sc.page_infos().len()))
1250
+ .ref_();
1251
+
1252
+ if struct_page_ref.is_none() {
1253
+ struct_page_ref = Some(page_ref);
1254
+ }
1255
+
1256
+ if parent_tree_map.contains_key(&pi.into()) {
1257
+ panic!("the identifier {pi:?} appears twice in the tag tree");
1258
+ }
1259
+
1260
+ parent_tree_map.insert(pi.into(), parent_ref);
1261
+
1262
+ if struct_page_ref == Some(page_ref) {
1263
+ struct_children.marked_content_id(pi.mcid);
1264
+ } else {
1265
+ struct_children
1266
+ .marked_content_ref()
1267
+ .marked_content_id(pi.mcid)
1268
+ .page(page_ref);
1269
+ }
1270
+ }
1271
+ IdentifierType::AnnotationIdentifier(ai) => {
1272
+ let Some(page_info) = sc.page_infos_mut().get_mut(ai.page_index) else {
1273
+ panic!(
1274
+ "tag tree contains identifier from page {}, but document only has {} pages",
1275
+ ai.page_index + 1,
1276
+ sc.page_infos().len()
1277
+ );
1278
+ };
1279
+
1280
+ let page_ref = page_info.ref_();
1281
+ let Some((annotation_ref, struct_parent)) =
1282
+ page_info.annotations_mut().get_mut(ai.annot_index)
1283
+ else {
1284
+ panic!(
1285
+ "tag tree contains identifier from annotation {} on page {}, but page only has {} annotations",
1286
+ ai.annot_index + 1,
1287
+ ai.page_index + 1,
1288
+ page_info.annotations().len()
1289
+ );
1290
+ };
1291
+
1292
+ if parent_tree_map.contains_key(&ai.into()) {
1293
+ panic!("identifier {ai:?} appears twice in the tag tree");
1294
+ }
1295
+ parent_tree_map.insert(ai.into(), *annotation_ref);
1296
+
1297
+ struct_parent.set(parent_ref).expect("only one parent");
1298
+
1299
+ struct_children
1300
+ .object_ref()
1301
+ .page(page_ref)
1302
+ .object(*annotation_ref);
1303
+ }
1304
+ },
1305
+ }
1306
+ }
1307
+ struct_children.finish();
1308
+
1309
+ if let Some(spr) = struct_page_ref {
1310
+ struct_elem.page(spr);
1311
+ }
1312
+
1313
+ Ok(())
1314
+ }
1315
+
1316
+ /// Where a layout artifact is attached to the page edge.
1317
+ #[derive(Debug, Copy, Clone, Eq, PartialEq)]
1318
+ #[allow(missing_docs)]
1319
+ pub enum ArtifactAttachment {
1320
+ Left,
1321
+ Top,
1322
+ Right,
1323
+ Bottom,
1324
+ }