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.
- package/README.md +300 -10
- 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,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
|
+
}
|