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.
- package/README.md +266 -12
- package/THIRD_PARTY_NOTICES.md +4 -2
- package/dist/browser.d.ts +1 -1
- package/dist/browser.d.ts.map +1 -1
- package/dist/document-options.d.ts +36 -0
- package/dist/document-options.d.ts.map +1 -0
- package/dist/document-options.js +236 -0
- package/dist/document-options.js.map +1 -0
- package/dist/node.d.ts +1 -1
- package/dist/node.d.ts.map +1 -1
- package/dist/options.d.ts +35 -1
- package/dist/options.d.ts.map +1 -1
- package/dist/options.js +21 -14
- package/dist/options.js.map +1 -1
- package/dist/types.d.ts +71 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/validation.d.ts +2 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +7 -0
- package/dist/validation.js.map +1 -0
- package/examples/zugferd.mjs +41 -0
- package/licenses/RUST_DEPENDENCIES.txt +27 -0
- package/package.json +3 -2
- package/src/browser.ts +1 -1
- package/src/document-options.ts +218 -0
- package/src/node.ts +1 -1
- package/src/options.ts +18 -13
- package/src/types.ts +76 -1
- package/src/validation.ts +6 -0
- package/vendor/README.md +27 -2
- package/vendor/krilla/.cargo-ok +1 -0
- package/vendor/krilla/.cargo_vcs_info.json +6 -0
- package/vendor/krilla/Cargo.lock +1357 -0
- package/vendor/krilla/Cargo.toml +165 -0
- package/vendor/krilla/LICENSE_APACHE +202 -0
- package/vendor/krilla/LICENSE_MIT +21 -0
- package/vendor/krilla/README.md +156 -0
- package/vendor/krilla/examples/basic.rs +117 -0
- package/vendor/krilla/examples/empty_document.rs +25 -0
- package/vendor/krilla/examples/parley.rs +174 -0
- package/vendor/krilla/examples/simple_text.rs +118 -0
- package/vendor/krilla/examples/stream_builder.rs +93 -0
- package/vendor/krilla/icc/LICENSE +116 -0
- package/vendor/krilla/icc/README.md +1 -0
- package/vendor/krilla/icc/sGrey-v2-magic.icc +0 -0
- package/vendor/krilla/icc/sGrey-v4.icc +0 -0
- package/vendor/krilla/icc/sRGB-v2-magic.icc +0 -0
- package/vendor/krilla/icc/sRGB-v4.icc +0 -0
- package/vendor/krilla/src/chunk_container.rs +503 -0
- package/vendor/krilla/src/configure/PDF_A1.md +227 -0
- package/vendor/krilla/src/configure/PDF_A2.md +269 -0
- package/vendor/krilla/src/configure/PDF_A3.md +31 -0
- package/vendor/krilla/src/configure/PDF_A4.md +211 -0
- package/vendor/krilla/src/configure/PDF_UA1.md +202 -0
- package/vendor/krilla/src/configure/README.md +23 -0
- package/vendor/krilla/src/configure/mod.rs +197 -0
- package/vendor/krilla/src/configure/validate.rs +1244 -0
- package/vendor/krilla/src/configure/version.rs +134 -0
- package/vendor/krilla/src/content.rs +1352 -0
- package/vendor/krilla/src/data.rs +49 -0
- package/vendor/krilla/src/document.rs +156 -0
- package/vendor/krilla/src/error.rs +170 -0
- package/vendor/krilla/src/geom.rs +341 -0
- package/vendor/krilla/src/graphics/blend.rs +46 -0
- package/vendor/krilla/src/graphics/color.rs +552 -0
- package/vendor/krilla/src/graphics/graphic.rs +30 -0
- package/vendor/krilla/src/graphics/graphics_state.rs +251 -0
- package/vendor/krilla/src/graphics/icc.rs +218 -0
- package/vendor/krilla/src/graphics/image.rs +835 -0
- package/vendor/krilla/src/graphics/mask.rs +128 -0
- package/vendor/krilla/src/graphics/mod.rs +14 -0
- package/vendor/krilla/src/graphics/paint.rs +434 -0
- package/vendor/krilla/src/graphics/separation.rs +86 -0
- package/vendor/krilla/src/graphics/shading_function.rs +865 -0
- package/vendor/krilla/src/graphics/shading_pattern.rs +60 -0
- package/vendor/krilla/src/graphics/tiling_pattern.rs +119 -0
- package/vendor/krilla/src/graphics/xobject.rs +150 -0
- package/vendor/krilla/src/interactive/action.rs +68 -0
- package/vendor/krilla/src/interactive/annotation.rs +283 -0
- package/vendor/krilla/src/interactive/destination.rs +289 -0
- package/vendor/krilla/src/interactive/mod.rs +3 -0
- package/vendor/krilla/src/interchange/embed.rs +240 -0
- package/vendor/krilla/src/interchange/metadata.rs +564 -0
- package/vendor/krilla/src/interchange/mod.rs +4 -0
- package/vendor/krilla/src/interchange/outline.rs +263 -0
- package/vendor/krilla/src/interchange/tagging/fmt.rs +733 -0
- package/vendor/krilla/src/interchange/tagging/generate.toml +474 -0
- package/vendor/krilla/src/interchange/tagging/generated.rs +4754 -0
- package/vendor/krilla/src/interchange/tagging/mod.rs +1324 -0
- package/vendor/krilla/src/interchange/tagging/tag.rs +686 -0
- package/vendor/krilla/src/lib.rs +179 -0
- package/vendor/krilla/src/num.rs +26 -0
- package/vendor/krilla/src/page.rs +612 -0
- package/vendor/krilla/src/pdf.rs +313 -0
- package/vendor/krilla/src/resource.rs +436 -0
- package/vendor/krilla/src/serialize.rs +1101 -0
- package/vendor/krilla/src/stream.rs +368 -0
- package/vendor/krilla/src/surface.rs +664 -0
- package/vendor/krilla/src/text/cid.rs +488 -0
- package/vendor/krilla/src/text/font.rs +351 -0
- package/vendor/krilla/src/text/glyph/bitmap.rs +81 -0
- package/vendor/krilla/src/text/glyph/colr.rs +581 -0
- package/vendor/krilla/src/text/glyph/mod.rs +205 -0
- package/vendor/krilla/src/text/glyph/outline.rs +72 -0
- package/vendor/krilla/src/text/glyph/svg.rs +34 -0
- package/vendor/krilla/src/text/group.rs +463 -0
- package/vendor/krilla/src/text/mod.rs +232 -0
- package/vendor/krilla/src/text/shape.rs +102 -0
- package/vendor/krilla/src/text/type3.rs +514 -0
- package/vendor/krilla/src/util.rs +286 -0
- package/vendor/stylo/properties/longhands.toml +0 -1
- package/vendor/stylo/stylesheets/rule_parser.rs +1 -1
- package/wasm/blitz_pdf.js +10 -5
- 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
|
+
}
|