blitz-pdf 0.1.0 → 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 +300 -10
  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,60 @@
1
+ use std::hash::Hash;
2
+ use std::sync::Arc;
3
+
4
+ use pdf_writer::{Finish, Name, Ref};
5
+
6
+ use crate::chunk_container::ChunkContainer;
7
+ use crate::geom::Transform;
8
+ use crate::graphics::shading_function::{GradientProperties, ShadingFunction};
9
+ use crate::resource;
10
+ use crate::resource::Resourceable;
11
+ use crate::serialize::{Cacheable, SerializeContext};
12
+
13
+ #[derive(Debug, PartialEq)]
14
+ struct Repr {
15
+ shading_function: ShadingFunction,
16
+ shading_transform: Transform,
17
+ }
18
+
19
+ impl Eq for Repr {}
20
+
21
+ impl Hash for Repr {
22
+ fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
23
+ self.shading_function.hash(state);
24
+ self.shading_transform.hash(state);
25
+ }
26
+ }
27
+
28
+ #[derive(Debug, Hash, Eq, PartialEq, Clone)]
29
+ pub(crate) struct ShadingPattern(Arc<Repr>);
30
+
31
+ impl ShadingPattern {
32
+ pub fn new(gradient_properties: GradientProperties, shading_transform: Transform) -> Self {
33
+ Self(Arc::new(Repr {
34
+ // CTM doesn't need to be included to calculate the domain of the shading function
35
+ shading_function: ShadingFunction::new(gradient_properties, false),
36
+ shading_transform,
37
+ }))
38
+ }
39
+ }
40
+
41
+ impl Cacheable for ShadingPattern {
42
+ fn serialize(
43
+ self,
44
+ sc: &mut SerializeContext,
45
+ chunk_container: &mut ChunkContainer,
46
+ root_ref: Ref,
47
+ ) {
48
+ let shading_ref = sc.register_cacheable(chunk_container, self.0.shading_function.clone());
49
+ let chunk = &mut chunk_container.non_stream.patterns;
50
+ let mut shading_pattern = chunk.shading_pattern(root_ref);
51
+ shading_pattern.pair(Name(b"Shading"), shading_ref);
52
+ shading_pattern.matrix(self.0.shading_transform.to_pdf_transform());
53
+
54
+ shading_pattern.finish();
55
+ }
56
+ }
57
+
58
+ impl Resourceable for ShadingPattern {
59
+ type Resource = resource::Pattern;
60
+ }
@@ -0,0 +1,119 @@
1
+ //! Tiling patterns.
2
+
3
+ use std::hash::{Hash, Hasher};
4
+ use std::ops::DerefMut;
5
+
6
+ use pdf_writer::types::{PaintType, TilingType};
7
+ use pdf_writer::{Finish, Ref};
8
+
9
+ use crate::chunk_container::ChunkContainer;
10
+ use crate::geom::Transform;
11
+ use crate::num::NormalizedF32;
12
+ use crate::resource;
13
+ use crate::resource::Resourceable;
14
+ use crate::serialize::{Cacheable, SerializeContext};
15
+ use crate::stream::StreamBuilder;
16
+ use crate::stream::{FilterStreamBuilder, Stream};
17
+
18
+ #[derive(Debug, PartialEq)]
19
+ pub(crate) struct TilingPattern {
20
+ stream: Stream,
21
+ transform: Transform,
22
+ base_opacity: NormalizedF32,
23
+ width: f32,
24
+ height: f32,
25
+ }
26
+
27
+ impl Eq for TilingPattern {}
28
+
29
+ impl Hash for TilingPattern {
30
+ fn hash<H: Hasher>(&self, state: &mut H) {
31
+ self.stream.hash(state);
32
+ self.transform.hash(state);
33
+ self.base_opacity.hash(state);
34
+ self.width.to_bits().hash(state);
35
+ self.height.to_bits().hash(state);
36
+ }
37
+ }
38
+
39
+ impl TilingPattern {
40
+ pub(crate) fn new(
41
+ stream: Stream,
42
+ transform: Transform,
43
+ base_opacity: NormalizedF32,
44
+ width: f32,
45
+ height: f32,
46
+ serializer_context: &mut SerializeContext,
47
+ chunk_container: &mut ChunkContainer,
48
+ ) -> Self {
49
+ // stroke/fill opacity doesn't work consistently across different viewers for patterns,
50
+ // so instead we simulate it ourselves.
51
+ let pattern_stream = if base_opacity == NormalizedF32::ONE {
52
+ stream
53
+ } else {
54
+ let stream = {
55
+ let mut builder = StreamBuilder::new(serializer_context, chunk_container);
56
+ let mut surface = builder.surface();
57
+ surface.draw_opacified_stream(base_opacity, stream);
58
+ surface.finish();
59
+ builder.finish()
60
+ };
61
+
62
+ stream
63
+ };
64
+
65
+ Self {
66
+ stream: pattern_stream,
67
+ transform,
68
+ base_opacity,
69
+ width,
70
+ height,
71
+ }
72
+ }
73
+ }
74
+
75
+ impl Cacheable for TilingPattern {
76
+ fn serialize(
77
+ self,
78
+ sc: &mut SerializeContext,
79
+ chunk_container: &mut ChunkContainer,
80
+ root_ref: Ref,
81
+ ) {
82
+ let mut chunk = sc.new_chunk();
83
+
84
+ for validation_error in self.stream.validation_errors {
85
+ sc.register_validation_error(validation_error);
86
+ }
87
+
88
+ let pattern_stream = FilterStreamBuilder::new_from_content_stream(
89
+ &self.stream.content,
90
+ &sc.serialize_settings(),
91
+ )
92
+ .finish(&sc.serialize_settings());
93
+ let mut tiling_pattern = chunk.tiling_pattern(root_ref, pattern_stream.encoded_data());
94
+ pattern_stream.write_filters(tiling_pattern.deref_mut().deref_mut());
95
+
96
+ self.stream.resource_dictionary.to_pdf_resources(
97
+ &mut tiling_pattern,
98
+ sc,
99
+ &mut chunk_container.non_stream.resource_dictionaries,
100
+ );
101
+
102
+ let final_bbox = pdf_writer::Rect::new(0.0, 0.0, self.width, self.height);
103
+
104
+ tiling_pattern
105
+ .tiling_type(TilingType::ConstantSpacing)
106
+ .paint_type(PaintType::Colored)
107
+ .bbox(final_bbox)
108
+ .matrix(self.transform.to_pdf_transform())
109
+ .x_step(final_bbox.x2 - final_bbox.x1)
110
+ .y_step(final_bbox.y2 - final_bbox.y1);
111
+
112
+ tiling_pattern.finish();
113
+ chunk_container.streams.patterns.push(chunk);
114
+ }
115
+ }
116
+
117
+ impl Resourceable for TilingPattern {
118
+ type Resource = resource::Pattern;
119
+ }
@@ -0,0 +1,150 @@
1
+ use std::ops::DerefMut;
2
+ use std::sync::Arc;
3
+
4
+ use pdf_writer::{Finish, Name, Ref};
5
+
6
+ use crate::chunk_container::ChunkContainer;
7
+ use crate::configure::ValidationError;
8
+ use crate::geom::Rect;
9
+ use crate::graphics::color::{rgb, DEVICE_RGB};
10
+ use crate::resource;
11
+ use crate::resource::{Resource, Resourceable};
12
+ use crate::serialize::{Cacheable, MaybeDeviceColorSpace, SerializeContext};
13
+ use crate::stream::{FilterStreamBuilder, Stream};
14
+ use crate::util::{NameExt, Prehashed};
15
+
16
+ #[derive(Debug, Hash, Eq, PartialEq)]
17
+ struct Repr {
18
+ stream: Stream,
19
+ isolated: bool,
20
+ transparency_group_color_space: bool,
21
+ custom_bbox: Option<Rect>,
22
+ }
23
+
24
+ #[derive(Debug, Hash, Clone, Eq, PartialEq)]
25
+ pub(crate) struct XObject(Arc<Prehashed<Repr>>);
26
+
27
+ impl XObject {
28
+ pub(crate) fn new(
29
+ stream: Stream,
30
+ isolated: bool,
31
+ mut transparency_group_color_space: bool,
32
+ custom_bbox: Option<Rect>,
33
+ ) -> Self {
34
+ // In case a mask was invoked in the content stream, we _always_ create
35
+ // a new transparency group. Please see <https://github.com/typst/typst/issues/5509>.
36
+ // Just to provide a brief explanation: I have not found any mention
37
+ // of a transparency group being required when using a soft mask in the
38
+ // PDF spec. However, Apple Preview (and only them!) have a weird bug
39
+ // where if you transform an XObject with a soft mask but not transparency
40
+ // group, the transform gets applied twice to the mask, resulting in
41
+ // rendering issues. Using a transparency group in this case seems to
42
+ // fix the issue.
43
+ // TODO: Apply group transparency to page as well.
44
+ transparency_group_color_space |= stream.uses_mask;
45
+
46
+ Self(Arc::new(Prehashed::new(Repr {
47
+ stream,
48
+ isolated,
49
+ transparency_group_color_space,
50
+ custom_bbox,
51
+ })))
52
+ }
53
+
54
+ pub(crate) fn is_empty(&self) -> bool {
55
+ self.0.stream.is_empty()
56
+ }
57
+
58
+ pub(crate) fn bbox(&self) -> Rect {
59
+ self.0.custom_bbox.unwrap_or(self.0.stream.bbox)
60
+ }
61
+ }
62
+
63
+ impl Cacheable for XObject {
64
+ fn serialize(
65
+ self,
66
+ sc: &mut SerializeContext,
67
+ chunk_container: &mut ChunkContainer,
68
+ root_ref: Ref,
69
+ ) {
70
+ let mut chunk = sc.new_chunk();
71
+
72
+ for validation_error in &self.0.stream.validation_errors {
73
+ sc.register_validation_error(validation_error.clone());
74
+ }
75
+
76
+ let use_transparency_group = self.0.isolated || self.0.transparency_group_color_space;
77
+
78
+ if use_transparency_group {
79
+ sc.register_validation_error(ValidationError::Transparency(sc.location));
80
+ }
81
+
82
+ let serialize_settings = sc.serialize_settings();
83
+
84
+ // "Ordinarily, the CS entry may be present only for isolated transparency groups (those
85
+ // for which I is true), and even then it is optional. However, this entry shall be present in
86
+ // the group attributes dictionary for any transparency group XObject that has no parent
87
+ // group or page from which to inherit."
88
+ //
89
+ // So while we don't technically always need to write it, let's just play it save
90
+ // and always add a group color space in case we have a transparency group.
91
+ let transparency_group_cs = if use_transparency_group {
92
+ Some(sc.register_colorspace(
93
+ chunk_container,
94
+ rgb::color_space(serialize_settings.no_device_cs).into(),
95
+ ))
96
+ } else {
97
+ None
98
+ };
99
+
100
+ let x_object_stream = FilterStreamBuilder::new_from_content_stream(
101
+ &self.0.stream.content,
102
+ &serialize_settings,
103
+ )
104
+ .finish(&serialize_settings);
105
+ let mut x_object = chunk.form_xobject(root_ref, x_object_stream.encoded_data());
106
+ x_object_stream.write_filters(x_object.deref_mut().deref_mut());
107
+
108
+ self.0.stream.resource_dictionary.to_pdf_resources(
109
+ &mut x_object,
110
+ sc,
111
+ &mut chunk_container.non_stream.resource_dictionaries,
112
+ );
113
+ x_object.bbox(
114
+ self.0
115
+ .custom_bbox
116
+ .unwrap_or(self.0.stream.bbox)
117
+ .to_pdf_rect(),
118
+ );
119
+
120
+ if use_transparency_group {
121
+ let mut group = x_object.group();
122
+ let transparency = group.transparency();
123
+
124
+ if self.0.isolated {
125
+ transparency.isolated(self.0.isolated);
126
+ }
127
+
128
+ if let Some(transparency_group_cs) = transparency_group_cs {
129
+ let pdf_cs = transparency.insert(Name(b"CS"));
130
+
131
+ match transparency_group_cs {
132
+ MaybeDeviceColorSpace::DeviceRgb => pdf_cs.primitive(DEVICE_RGB.to_pdf_name()),
133
+ // Can only be SRGB
134
+ MaybeDeviceColorSpace::ColorSpace(cs) => pdf_cs.primitive(cs.get_ref()),
135
+ _ => unreachable!(),
136
+ }
137
+ }
138
+
139
+ transparency.finish();
140
+ group.finish();
141
+ }
142
+
143
+ x_object.finish();
144
+ chunk_container.streams.x_objects.push(chunk);
145
+ }
146
+ }
147
+
148
+ impl Resourceable for XObject {
149
+ type Resource = resource::XObject;
150
+ }
@@ -0,0 +1,68 @@
1
+ //! PDF actions, allowing you to add interactivity to the document.
2
+ //!
3
+ //! PDF has the concept of "actions", which encompass things like navigating to a URL,
4
+ //! opening some file on the system, and so on. The PDF reference defines a whole bunch
5
+ //! of actions, but krilla does not expose nearly all of them, and never will. As of right now,
6
+ //! the only available action is the link action, which allows you to specify a link that
7
+ //! should be opened, when activating the action.
8
+
9
+ use pdf_writer::types::ActionType;
10
+ use pdf_writer::{Name, Str};
11
+
12
+ use crate::error::KrillaResult;
13
+ use crate::interactive::destination::Destination;
14
+ use crate::serialize::SerializeContext;
15
+
16
+ /// A type of action.
17
+ pub enum Action {
18
+ /// A link action.
19
+ Link(LinkAction),
20
+ /// A go-to action.
21
+ Goto(Destination),
22
+ }
23
+
24
+ impl Action {
25
+ pub(crate) fn serialize(
26
+ &self,
27
+ sc: &mut SerializeContext,
28
+ mut action: pdf_writer::writers::Action,
29
+ ) -> KrillaResult<()> {
30
+ match self {
31
+ Action::Link(link) => {
32
+ link.serialize(action);
33
+
34
+ Ok(())
35
+ }
36
+ Action::Goto(dest) => {
37
+ let dest_entry = action.action_type(ActionType::GoTo).insert(Name(b"D"));
38
+ dest.serialize(sc, dest_entry)
39
+ }
40
+ }
41
+ }
42
+ }
43
+
44
+ /// A link action. Will open a link when clicked.
45
+ pub struct LinkAction {
46
+ uri: String,
47
+ }
48
+
49
+ impl From<LinkAction> for Action {
50
+ fn from(value: LinkAction) -> Self {
51
+ Action::Link(value)
52
+ }
53
+ }
54
+
55
+ impl LinkAction {
56
+ /// Create a new link action that will open a URI when clicked.
57
+ pub fn new(uri: String) -> Self {
58
+ Self { uri }
59
+ }
60
+ }
61
+
62
+ impl LinkAction {
63
+ fn serialize(&self, mut action: pdf_writer::writers::Action) {
64
+ action
65
+ .action_type(ActionType::Uri)
66
+ .uri(Str(self.uri.as_bytes()));
67
+ }
68
+ }
@@ -0,0 +1,283 @@
1
+ //! PDF annotations, allowing you to add extra "content" to specific pages.
2
+ //!
3
+ //! PDF has the concept of annotations, which allow you to associate certain regions of
4
+ //! a page with an "annotation". The PDF reference defines many different actions, however,
5
+ //! krilla does not and never will expose all of them. As of right now, the only annotations
6
+ //! that are supported are "link annotations", which allow you associate a certain region of
7
+ //! the page with a link.
8
+
9
+ use core::f32;
10
+
11
+ use pdf_writer::types::AnnotationFlags;
12
+ use pdf_writer::{Finish, Name, Ref, TextStr};
13
+
14
+ use crate::chunk_container::ChunkContainer;
15
+ use crate::color::Color;
16
+ use crate::configure::{PdfVersion, ValidationError};
17
+ use crate::error::KrillaResult;
18
+ use crate::geom::{Quadrilateral, Rect};
19
+ use crate::interactive::action::Action;
20
+ use crate::interactive::destination::Destination;
21
+ use crate::page::page_root_transform;
22
+ use crate::serialize::SerializeContext;
23
+ use crate::surface::Location;
24
+
25
+ /// An annotation.
26
+ pub struct Annotation {
27
+ pub(crate) annotation_type: AnnotationType,
28
+ pub(crate) alt: Option<String>,
29
+ pub(crate) struct_parent: Option<i32>,
30
+ pub(crate) location: Option<Location>,
31
+ }
32
+
33
+ impl Annotation {
34
+ /// Create a new link annotation with some alt text.
35
+ ///
36
+ /// Note that the alt text might be required in some cases, for example
37
+ /// when exporting to PDF/UA.
38
+ pub fn new_link(annotation: LinkAnnotation, alt_text: Option<String>) -> Self {
39
+ Self {
40
+ annotation_type: AnnotationType::Link(annotation),
41
+ alt: alt_text,
42
+ struct_parent: None,
43
+ location: None,
44
+ }
45
+ }
46
+
47
+ /// Sets the location of the annotation.
48
+ pub fn with_location(mut self, location: Option<Location>) -> Self {
49
+ self.location = location;
50
+ self
51
+ }
52
+ }
53
+
54
+ impl From<LinkAnnotation> for Annotation {
55
+ fn from(value: LinkAnnotation) -> Self {
56
+ Self {
57
+ annotation_type: AnnotationType::Link(value),
58
+ alt: None,
59
+ struct_parent: None,
60
+ location: None,
61
+ }
62
+ }
63
+ }
64
+
65
+ impl Annotation {
66
+ pub(crate) fn serialize(
67
+ &self,
68
+ sc: &mut SerializeContext,
69
+ chunk_container: &mut ChunkContainer,
70
+ root_ref: Ref,
71
+ page_height: f32,
72
+ ) -> KrillaResult<()> {
73
+ let chunk = &mut chunk_container.non_stream.annotations;
74
+ let mut annotation = chunk
75
+ .indirect(root_ref)
76
+ .start::<pdf_writer::writers::Annotation>();
77
+
78
+ self.annotation_type
79
+ .serialize_type(sc, &mut annotation, page_height)?;
80
+
81
+ let AnnotationType::Link(l) = &self.annotation_type;
82
+ // Only set the print flag when really necessary (only PDF/A). Don't
83
+ // set it by default, so annotations with color borders will be shown
84
+ // on a screen but not printed.
85
+ // TODO: No need to write the print flag even if it is `None`,
86
+ // only for PDF/A.
87
+ if l.border.is_none()
88
+ || sc
89
+ .serialize_settings()
90
+ .configuration
91
+ .validators()
92
+ .requires_annotation_flags()
93
+ {
94
+ annotation.flags(AnnotationFlags::PRINT);
95
+ }
96
+
97
+ if let Some(struct_parent) = self.struct_parent {
98
+ annotation.struct_parent(struct_parent);
99
+ }
100
+
101
+ if let Some(alt_text) = &self.alt {
102
+ annotation.contents(TextStr(alt_text));
103
+ }
104
+
105
+ if self.alt.as_ref().is_none_or(String::is_empty) {
106
+ sc.register_validation_error(ValidationError::MissingAnnotationAltText(self.location));
107
+ }
108
+
109
+ annotation.finish();
110
+
111
+ Ok(())
112
+ }
113
+ }
114
+
115
+ /// A type of annotation.
116
+ pub enum AnnotationType {
117
+ /// A link annotation.
118
+ Link(LinkAnnotation),
119
+ }
120
+
121
+ impl AnnotationType {
122
+ fn serialize_type(
123
+ &self,
124
+ sc: &mut SerializeContext,
125
+ annotation: &mut pdf_writer::writers::Annotation,
126
+ page_height: f32,
127
+ ) -> KrillaResult<()> {
128
+ match self {
129
+ AnnotationType::Link(l) => l.serialize_type(sc, annotation, page_height),
130
+ }
131
+ }
132
+ }
133
+
134
+ /// An annotation target.
135
+ pub enum Target {
136
+ /// A destination within the document.
137
+ Destination(Destination),
138
+ /// An action to be performed.
139
+ Action(Action),
140
+ }
141
+
142
+ /// Border of a link annotation.
143
+ pub struct LinkBorder {
144
+ pub(crate) width: f32,
145
+ pub(crate) color: Color,
146
+ }
147
+
148
+ impl LinkBorder {
149
+ /// Create a new link annotation border.
150
+ ///
151
+ /// `width`: The width of the border in pt.
152
+ /// `color`: The color of the border.
153
+ pub fn new(width: f32, color: Color) -> Self {
154
+ Self { width, color }
155
+ }
156
+ }
157
+
158
+ /// A link annotation.
159
+ pub struct LinkAnnotation {
160
+ pub(crate) rect: Rect,
161
+ pub(crate) quad_points: Option<Vec<Quadrilateral>>,
162
+ pub(crate) target: Target,
163
+ pub(crate) border: Option<LinkBorder>,
164
+ }
165
+
166
+ impl LinkAnnotation {
167
+ /// Create a new link annotation.
168
+ ///
169
+ /// `rect`: The bounding box of the link annotation that it should cover on the page.
170
+ /// `target`: The target of the link annotation.
171
+ pub fn new(rect: Rect, target: Target) -> Self {
172
+ Self {
173
+ rect,
174
+ quad_points: None,
175
+ target,
176
+ border: None,
177
+ }
178
+ }
179
+
180
+ /// Create a new link annotation.
181
+ ///
182
+ /// `target`: The target of the link annotation.
183
+ /// `quad_points`: An array of quadrilaterals that define where the link
184
+ /// annotation should be activated. This is useful if you for example have
185
+ /// a link annotation that is broken to one or multiple lines.
186
+ pub fn new_with_quad_points(quad_points: Vec<Quadrilateral>, target: Target) -> Self {
187
+ assert!(!quad_points.is_empty());
188
+
189
+ let mut min_x = f32::INFINITY;
190
+ let mut min_y = f32::INFINITY;
191
+ let mut max_x = f32::NEG_INFINITY;
192
+ let mut max_y = f32::NEG_INFINITY;
193
+
194
+ for point in quad_points.iter().flat_map(|q| q.0) {
195
+ min_x = min_x.min(point.x);
196
+ min_y = min_y.min(point.y);
197
+ max_x = max_x.max(point.x);
198
+ max_y = max_y.max(point.y);
199
+ }
200
+
201
+ // Expand the bounding box by a little. There is a bug in adobe acrobat
202
+ // that sometimes prevents the quadpoints from being used if the quad
203
+ // points lie exactly on the bounding rectangle.
204
+ const EPSILON: f32 = 0.001;
205
+ let rect = Rect::from_ltrb(
206
+ min_x - EPSILON,
207
+ min_y - EPSILON,
208
+ max_x + EPSILON,
209
+ max_y + EPSILON,
210
+ )
211
+ .unwrap();
212
+
213
+ Self {
214
+ rect,
215
+ quad_points: Some(quad_points),
216
+ target,
217
+ border: None,
218
+ }
219
+ }
220
+
221
+ /// Set a border for this link annotation. The border will be visible on
222
+ /// screen but not when printed, unless when exporting with PDF/A standard.
223
+ pub fn with_border(self, border: LinkBorder) -> Self {
224
+ Self {
225
+ border: Some(border),
226
+ ..self
227
+ }
228
+ }
229
+
230
+ fn serialize_type(
231
+ &self,
232
+ sc: &mut SerializeContext,
233
+ annotation: &mut pdf_writer::writers::Annotation,
234
+ page_height: f32,
235
+ ) -> KrillaResult<()> {
236
+ annotation.subtype(pdf_writer::types::AnnotationType::Link);
237
+
238
+ let actual_rect = self
239
+ .rect
240
+ .transform(page_root_transform(page_height))
241
+ .unwrap();
242
+ annotation.rect(actual_rect.to_pdf_rect());
243
+ annotation.border(
244
+ 0.0,
245
+ 0.0,
246
+ self.border.as_ref().map_or(0.0, |x| x.width),
247
+ None,
248
+ );
249
+
250
+ if let Some(border) = &self.border {
251
+ match border.color.to_regular() {
252
+ crate::color::RegularColor::Rgb(rgb) => {
253
+ let [r, g, b] = rgb.to_pdf_color();
254
+ annotation.color_rgb(r, g, b);
255
+ }
256
+ crate::color::RegularColor::Cmyk(cmyk) => {
257
+ let [c, m, y, k] = cmyk.to_pdf_color();
258
+ annotation.color_cmyk(c, m, y, k);
259
+ }
260
+ crate::color::RegularColor::Luma(gray) => {
261
+ annotation.color_gray(gray.to_pdf_color());
262
+ }
263
+ }
264
+ }
265
+
266
+ if sc.serialize_settings().pdf_version() >= PdfVersion::Pdf16 {
267
+ self.quad_points.as_ref().map(|p| {
268
+ annotation.quad_points(p.iter().flat_map(|q| q.0).flat_map(|p| {
269
+ let mut p = p.to_tsp();
270
+ page_root_transform(page_height).to_tsp().map_point(&mut p);
271
+ [p.x, p.y]
272
+ }))
273
+ });
274
+ }
275
+
276
+ match &self.target {
277
+ Target::Destination(destination) => {
278
+ destination.serialize(sc, annotation.insert(Name(b"Dest")))
279
+ }
280
+ Target::Action(action) => action.serialize(sc, annotation.action()),
281
+ }
282
+ }
283
+ }