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,1352 @@
1
+ //! A low-level abstraction over a single content stream.
2
+
3
+ use std::cell::RefCell;
4
+ use std::collections::HashSet;
5
+ use std::rc::Rc;
6
+ use std::sync::Arc;
7
+
8
+ use float_cmp::approx_eq;
9
+ use pdf_writer::types::TextRenderingMode;
10
+ use pdf_writer::{Content, Finish, Name, Ref, Str, TextStr};
11
+ use tiny_skia_path::{Path, PathSegment, PathVerb};
12
+
13
+ use crate::chunk_container::ChunkContainer;
14
+ use crate::color::rgb;
15
+ use crate::configure::ValidationError;
16
+ #[cfg(feature = "raster-images")]
17
+ use crate::geom::Size;
18
+ use crate::geom::{Point, Rect, Transform};
19
+ use crate::graphics::color::{Color, ColorSpace};
20
+ use crate::graphics::graphics_state::{ExtGState, GraphicsStates};
21
+ #[cfg(feature = "raster-images")]
22
+ use crate::graphics::image::Image;
23
+ use crate::graphics::mask::Mask;
24
+ use crate::graphics::paint::{Fill, FillRule, InnerPaint, LineCap, LineJoin, Paint, Stroke};
25
+ use crate::graphics::shading_function::{
26
+ GradientProperties, GradientPropertiesExt, ShadingFunction,
27
+ };
28
+ use crate::graphics::shading_pattern::ShadingPattern;
29
+ use crate::graphics::tiling_pattern::TilingPattern;
30
+ use crate::graphics::xobject::XObject;
31
+ use crate::interchange::tagging::ContentTag;
32
+ use crate::num::NormalizedF32;
33
+ use crate::resource;
34
+ use crate::resource::{Resource, ResourceDictionaryBuilder};
35
+ use crate::serialize::{MaybeDeviceColorSpace, SerializeContext};
36
+ use crate::stream::Stream;
37
+ use crate::text::group::{use_text_spanner, GlyphGroup, GlyphGrouper, GlyphSpan, GlyphSpanner};
38
+ use crate::text::type3::ColoredGlyph;
39
+ use crate::text::{Font, FontContainer, FontIdentifier, PdfFont, PDF_UNITS_PER_EM};
40
+ use crate::text::{Glyph, GlyphId};
41
+ use crate::util::{calculate_stroke_bbox, NameExt};
42
+
43
+ pub(crate) struct ContentBuilder {
44
+ rd_builder: ResourceDictionaryBuilder,
45
+ content: Content,
46
+ validation_errors: HashSet<ValidationError>,
47
+ root_transform: Transform,
48
+ graphics_states: GraphicsStates,
49
+ bbox: Option<Rect>,
50
+ uses_mask: bool,
51
+ // Calculating the bbox of text is expensive, so we should avoid doing it if not needed.
52
+ // The only time we really need it is if we are currently inside of an XObject, where we
53
+ // need to provide a bbox of all its contents. If we are on the main page stream, we only
54
+ // need it if automatic size detection is enabled.
55
+ bbox_important: bool,
56
+ /// A temporary buffer that's reused across the builder.
57
+ scratch: Vec<u8>,
58
+ pub(crate) active_marked_content: bool,
59
+ }
60
+
61
+ /// Stores either a device-specific color space,
62
+ /// or the name of a different colorspace (e.g. ICCBased) stored in
63
+ /// the current resource dictionary
64
+ enum ContentColorSpace {
65
+ Device,
66
+ Named(String),
67
+ }
68
+
69
+ impl ContentBuilder {
70
+ pub(crate) fn new(
71
+ root_transform: Transform,
72
+ bbox_important: bool,
73
+ sc: &SerializeContext,
74
+ ) -> Self {
75
+ Self {
76
+ rd_builder: ResourceDictionaryBuilder::new(),
77
+ validation_errors: HashSet::new(),
78
+ content: sc.new_content(),
79
+ root_transform,
80
+ uses_mask: false,
81
+ bbox_important,
82
+ graphics_states: GraphicsStates::new(),
83
+ bbox: None,
84
+ scratch: Vec::new(),
85
+ active_marked_content: false,
86
+ }
87
+ }
88
+
89
+ pub(crate) fn content_save_state(&mut self) {
90
+ self.content.save_state();
91
+
92
+ if self.content.state_nesting_depth() > 28 {
93
+ self.validation_errors
94
+ .insert(ValidationError::TooHighQNestingLevel);
95
+ }
96
+ }
97
+
98
+ pub(crate) fn finish(self, sc: &mut SerializeContext) -> Stream {
99
+ let buf = self.content.finish();
100
+ sc.register_limits(buf.limits());
101
+
102
+ Stream::new(
103
+ buf.to_vec(),
104
+ self.bbox
105
+ .unwrap_or(Rect::from_xywh(0.0, 0.0, 1.0, 1.0).unwrap()),
106
+ self.validation_errors.into_iter().collect(),
107
+ self.rd_builder.finish(),
108
+ self.uses_mask,
109
+ )
110
+ }
111
+
112
+ fn start_marked_content_prelude(&mut self) {
113
+ if self.active_marked_content {
114
+ panic!("can't start marked content twice");
115
+ }
116
+
117
+ self.active_marked_content = true;
118
+ }
119
+
120
+ #[track_caller]
121
+ pub(crate) fn start_marked_content(&mut self, name: Name) {
122
+ self.start_marked_content_prelude();
123
+ self.content.begin_marked_content(name);
124
+ }
125
+
126
+ #[track_caller]
127
+ pub(crate) fn start_marked_content_with_properties(
128
+ &mut self,
129
+ sc: &mut SerializeContext,
130
+ mcid: Option<i32>,
131
+ tag: ContentTag,
132
+ ) {
133
+ self.start_marked_content_prelude();
134
+
135
+ let mut mc = self
136
+ .content
137
+ .begin_marked_content_with_properties(tag.name());
138
+ let mut properties = mc.properties();
139
+
140
+ if let Some(mcid) = mcid {
141
+ properties.pairs([(Name(b"MCID"), mcid)]);
142
+ }
143
+
144
+ // Page height extracted from transform and passed to function to allow
145
+ // its dependants to flip the y-axis, mirroring Krilla conventions.
146
+ let page_height = self.root_transform.ty();
147
+ tag.write_properties(sc, properties, page_height);
148
+ }
149
+
150
+ pub(crate) fn end_marked_content(&mut self) {
151
+ if !self.active_marked_content {
152
+ panic!("can't end marked content when none has been started");
153
+ }
154
+
155
+ self.content.end_marked_content();
156
+ self.active_marked_content = false;
157
+ }
158
+
159
+ pub(crate) fn concat_transform(&mut self, transform: &Transform) {
160
+ self.graphics_states.transform(*transform);
161
+ }
162
+
163
+ fn cur_transform_with_root_transform(&self) -> Transform {
164
+ self.root_transform.pre_concat(self.cur_transform())
165
+ }
166
+
167
+ pub(crate) fn cur_transform(&self) -> Transform {
168
+ self.graphics_states.cur().transform()
169
+ }
170
+
171
+ pub(crate) fn save_graphics_state(&mut self) {
172
+ self.graphics_states.save_state();
173
+ }
174
+
175
+ pub(crate) fn restore_graphics_state(&mut self) {
176
+ self.graphics_states.restore_state();
177
+ }
178
+
179
+ pub(crate) fn set_blend_mode(&mut self, blend_mode: pdf_writer::types::BlendMode) {
180
+ if blend_mode != pdf_writer::types::BlendMode::Normal {
181
+ let state = ExtGState::new().blend_mode(blend_mode);
182
+ self.graphics_states.combine(&state);
183
+ }
184
+ }
185
+
186
+ pub(crate) fn expand_bbox(&mut self, new_bbox: Rect) {
187
+ let new_bbox = self.graphics_states.transform_bbox(new_bbox);
188
+ if let Some(bbox) = &mut self.bbox {
189
+ bbox.expand(&new_bbox);
190
+ } else {
191
+ self.bbox = Some(new_bbox);
192
+ }
193
+ }
194
+
195
+ pub(crate) fn draw_path(
196
+ &mut self,
197
+ path: &Path,
198
+ mut fill: Option<&Fill>,
199
+ stroke: Option<&Stroke>,
200
+ sc: &mut SerializeContext,
201
+ chunk_container: &mut ChunkContainer,
202
+ ) {
203
+ if fill.is_none() && stroke.is_none() {
204
+ return;
205
+ }
206
+
207
+ // Zero-size geometry, don't draw.
208
+ if path.bounds().width() == 0.0 && path.bounds().height() == 0.0 {
209
+ return;
210
+ }
211
+
212
+ // See issue 199.
213
+ let is_line = path.verbs().len() == 2
214
+ && path.verbs()[0] == PathVerb::Move
215
+ && path.verbs()[1] == PathVerb::Line;
216
+ let dont_fill = path.bounds().width() == 0.0 || path.bounds().height() == 0.0 || is_line;
217
+
218
+ if dont_fill {
219
+ match stroke.is_some() {
220
+ // Some PDF viewers have bugs where they slightly fill zero-sized lines, so
221
+ // don't draw them in the first place.
222
+ false => return,
223
+ // Don't fill, but we still want to draw the stroke, so don't return.
224
+ true => {
225
+ fill = None;
226
+ }
227
+ }
228
+ }
229
+
230
+ let bbox_important = self.bbox_important;
231
+ let calculate_bbox = |is_solid: bool| bbox_important || !is_solid;
232
+
233
+ let stroke_bbox = |stroke: &Stroke| {
234
+ if calculate_bbox(matches!(&stroke.paint.0, InnerPaint::Color(_))) {
235
+ calculate_stroke_bbox(stroke, path).unwrap_or(Rect::from_tsp(path.bounds()))
236
+ } else {
237
+ Rect::from_tsp(path.bounds())
238
+ }
239
+ };
240
+
241
+ let fill_prep = |sb: &mut ContentBuilder, fill: &Fill| {
242
+ let has_pattern = matches!(fill.paint.0, InnerPaint::Pattern(_));
243
+ let fill_opacity = fill.opacity;
244
+ sb.expand_bbox(Rect::from_tsp(path.bounds()));
245
+
246
+ // PDF viewers don't show patterns with fill/stroke opacities consistently.
247
+ // Because of this, the opacity is accounted for in the pattern itself.
248
+ if !has_pattern {
249
+ sb.set_fill_opacity(fill_opacity);
250
+ }
251
+ };
252
+
253
+ let stroke_prep = |sb: &mut ContentBuilder, stroke: &Stroke, stroke_bbox: Rect| {
254
+ let is_pattern = matches!(stroke.paint.0, InnerPaint::Pattern(_));
255
+ let stroke_opacity = stroke.opacity;
256
+ sb.expand_bbox(stroke_bbox);
257
+
258
+ // See comment in `set_fill_properties`
259
+ if !is_pattern {
260
+ sb.set_stroke_opacity(stroke_opacity);
261
+ }
262
+ };
263
+
264
+ let fill_op = |sb: &mut ContentBuilder,
265
+ sc: &mut SerializeContext,
266
+ chunk_container: &mut ChunkContainer,
267
+ fill: &Fill| {
268
+ let fill_rule = fill.rule;
269
+ sb.content_set_fill_properties(
270
+ Rect::from_tsp(path.bounds()),
271
+ fill,
272
+ sc,
273
+ chunk_container,
274
+ );
275
+ sb.content_draw_path(path.segments());
276
+
277
+ match fill_rule {
278
+ FillRule::NonZero => sb.content.fill_nonzero(),
279
+ FillRule::EvenOdd => sb.content.fill_even_odd(),
280
+ };
281
+ };
282
+
283
+ let stroke_op = |sb: &mut ContentBuilder,
284
+ sc: &mut SerializeContext,
285
+ chunk_container: &mut ChunkContainer,
286
+ stroke: &Stroke,
287
+ stroke_bbox: Rect| {
288
+ sb.content_set_stroke_properties(stroke_bbox, stroke, sc, chunk_container);
289
+ sb.content_draw_path(path.segments());
290
+ sb.content.stroke();
291
+ };
292
+
293
+ let fill_stroke_op = |sb: &mut ContentBuilder,
294
+ sc: &mut SerializeContext,
295
+ chunk_container: &mut ChunkContainer,
296
+ fill: &Fill,
297
+ stroke: &Stroke,
298
+ stroke_bbox: Rect| {
299
+ let fill_rule = fill.rule;
300
+ sb.content_set_fill_properties(
301
+ Rect::from_tsp(path.bounds()),
302
+ fill,
303
+ sc,
304
+ chunk_container,
305
+ );
306
+ sb.content_set_stroke_properties(stroke_bbox, stroke, sc, chunk_container);
307
+ sb.content_draw_path(path.segments());
308
+
309
+ match fill_rule {
310
+ FillRule::NonZero => sb.content.fill_nonzero_and_stroke(),
311
+ FillRule::EvenOdd => sb.content.fill_even_odd_and_stroke(),
312
+ };
313
+ };
314
+
315
+ match (fill, stroke) {
316
+ (Some(fill), None) => {
317
+ self.apply_isolated_op(
318
+ |sb, _, _| {
319
+ fill_prep(sb, fill);
320
+ },
321
+ |sb, sc, chunk_container| {
322
+ fill_op(sb, sc, chunk_container, fill);
323
+ },
324
+ sc,
325
+ chunk_container,
326
+ );
327
+ }
328
+ (None, Some(stroke)) => {
329
+ let stroke_bbox = stroke_bbox(stroke);
330
+
331
+ self.apply_isolated_op(
332
+ |sb, _, _| {
333
+ stroke_prep(sb, stroke, stroke_bbox);
334
+ },
335
+ |sb, sc, chunk_container| {
336
+ stroke_op(sb, sc, chunk_container, stroke, stroke_bbox);
337
+ },
338
+ sc,
339
+ chunk_container,
340
+ );
341
+ }
342
+ (Some(fill), Some(stroke)) => {
343
+ let stroke_bbox = stroke_bbox(stroke);
344
+
345
+ self.apply_isolated_op(
346
+ |sb, _, _| {
347
+ fill_prep(sb, fill);
348
+ stroke_prep(sb, stroke, stroke_bbox);
349
+ },
350
+ |sb, sc, chunk_container| {
351
+ fill_stroke_op(sb, sc, chunk_container, fill, stroke, stroke_bbox);
352
+ },
353
+ sc,
354
+ chunk_container,
355
+ );
356
+ }
357
+ (None, None) => unreachable!(),
358
+ }
359
+ }
360
+
361
+ pub(crate) fn push_clip_path(&mut self, path: &Path, clip_rule: &FillRule) {
362
+ self.content_save_state();
363
+ self.content_draw_path(
364
+ path.clone()
365
+ .transform(self.cur_transform_with_root_transform().to_tsp())
366
+ .unwrap()
367
+ .segments(),
368
+ );
369
+
370
+ match clip_rule {
371
+ FillRule::NonZero => self.content.clip_nonzero(),
372
+ FillRule::EvenOdd => self.content.clip_even_odd(),
373
+ };
374
+
375
+ self.content.end_path();
376
+ }
377
+
378
+ pub(crate) fn pop_clip_path(&mut self) {
379
+ self.content.restore_state();
380
+ }
381
+
382
+ #[allow(clippy::too_many_arguments)]
383
+ pub(crate) fn draw_glyphs(
384
+ &mut self,
385
+ start: Point,
386
+ sc: &mut SerializeContext,
387
+ chunk_container: &mut ChunkContainer,
388
+ fill: Option<&Fill>,
389
+ stroke: Option<&Stroke>,
390
+ context_color: rgb::Color,
391
+ glyphs: &[impl Glyph],
392
+ font: Font,
393
+ text: &str,
394
+ font_size: f32,
395
+ ) {
396
+ if fill.is_none() && stroke.is_none() {
397
+ return;
398
+ }
399
+
400
+ let (x, y) = (start.x, start.y);
401
+ self.graphics_states.save_state();
402
+
403
+ // Calculating the glyphs bbox is very expensive but not always necessary, so omit
404
+ // if not needed.
405
+ let bbox_important = self.bbox_important;
406
+ let calculate_bbox = |is_solid: bool| bbox_important || !is_solid;
407
+
408
+ let fill_action = |sb: &mut ContentBuilder,
409
+ sc: &mut SerializeContext,
410
+ chunk_container: &mut ChunkContainer,
411
+ fill: &Fill| {
412
+ let bbox = if calculate_bbox(matches!(&fill.paint.0, InnerPaint::Color(_))) {
413
+ let bbox = get_glyphs_bbox(glyphs, x, y, font_size, font.clone());
414
+ sb.expand_bbox(bbox);
415
+ bbox
416
+ } else {
417
+ Rect::from_xywh(0.0, 0.0, 1.0, 1.0).unwrap()
418
+ };
419
+
420
+ sb.content_set_fill_properties(bbox, fill, sc, chunk_container)
421
+ };
422
+
423
+ let stroke_action = |sb: &mut ContentBuilder,
424
+ sc: &mut SerializeContext,
425
+ chunk_container: &mut ChunkContainer,
426
+ stroke: &Stroke| {
427
+ let bbox = if calculate_bbox(matches!(&stroke.paint.0, InnerPaint::Color(_))) {
428
+ // TODO: Bbox should also account for stroke.
429
+ let bbox = get_glyphs_bbox(glyphs, x, y, font_size, font.clone());
430
+ sb.expand_bbox(bbox);
431
+ bbox
432
+ } else {
433
+ Rect::from_xywh(0.0, 0.0, 1.0, 1.0).unwrap()
434
+ };
435
+
436
+ sb.content_set_stroke_properties(bbox, stroke, sc, chunk_container);
437
+ };
438
+
439
+ let set_fill_opacity = |sb: &mut ContentBuilder, fill: &Fill| {
440
+ // PDF viewers don't show patterns with fill/stroke opacities consistently.
441
+ // Because of this, the opacity is accounted for in the pattern itself.
442
+ if !matches!(&fill.paint.0, &InnerPaint::Pattern(_)) {
443
+ sb.set_fill_opacity(fill.opacity);
444
+ }
445
+ };
446
+
447
+ let set_stroke_opacity = |sb: &mut ContentBuilder, stroke: &Stroke| {
448
+ // PDF viewers don't show patterns with fill/stroke opacities consistently.
449
+ // Because of this, the opacity is accounted for in the pattern itself.
450
+ if !matches!(&stroke.paint.0, &InnerPaint::Pattern(_)) {
451
+ // PDF viewers don't show patterns with fill/stroke opacities consistently.
452
+ // Because of this, the opacity is accounted for in the pattern itself.
453
+ sb.set_stroke_opacity(stroke.opacity);
454
+
455
+ // See the comment in `stroke-action` for why we also set the fill opacity.
456
+ sb.set_fill_opacity(stroke.opacity);
457
+ }
458
+ };
459
+
460
+ match (fill, stroke) {
461
+ (Some(f), Some(s)) => {
462
+ set_fill_opacity(self, f);
463
+ set_stroke_opacity(self, s);
464
+
465
+ self.fill_stroke_glyph_run(
466
+ x,
467
+ y,
468
+ sc,
469
+ chunk_container,
470
+ TextRenderingMode::FillStroke,
471
+ |sb, sc, chunk_container| {
472
+ fill_action(sb, sc, chunk_container, f);
473
+ stroke_action(sb, sc, chunk_container, s);
474
+ },
475
+ glyphs,
476
+ font.clone(),
477
+ context_color,
478
+ text,
479
+ font_size,
480
+ );
481
+ }
482
+ (Some(f), None) => {
483
+ set_fill_opacity(self, f);
484
+
485
+ self.fill_stroke_glyph_run(
486
+ x,
487
+ y,
488
+ sc,
489
+ chunk_container,
490
+ TextRenderingMode::Fill,
491
+ |sb, sc, chunk_container| {
492
+ fill_action(sb, sc, chunk_container, f);
493
+ },
494
+ glyphs,
495
+ font.clone(),
496
+ context_color,
497
+ text,
498
+ font_size,
499
+ );
500
+ }
501
+ (None, Some(s)) => {
502
+ set_stroke_opacity(self, s);
503
+
504
+ self.fill_stroke_glyph_run(
505
+ x,
506
+ y,
507
+ sc,
508
+ chunk_container,
509
+ TextRenderingMode::Stroke,
510
+ |sb, sc, chunk_container| {
511
+ stroke_action(sb, sc, chunk_container, s);
512
+ },
513
+ glyphs,
514
+ font.clone(),
515
+ context_color,
516
+ text,
517
+ font_size,
518
+ );
519
+ }
520
+ (None, None) => unreachable!(),
521
+ }
522
+
523
+ self.graphics_states.restore_state();
524
+ }
525
+
526
+ /// Encode a successive sequence of glyphs that share the same properties and
527
+ /// can be encoded with one text showing operator.
528
+ #[allow(clippy::too_many_arguments)]
529
+ #[inline(always)]
530
+ fn encode_consecutive_glyph_run(
531
+ &mut self,
532
+ sc: &mut SerializeContext,
533
+ cur_x: &mut f32,
534
+ cur_y: f32,
535
+ font_identifier: FontIdentifier,
536
+ pdf_font: &dyn PdfFont,
537
+ size: f32,
538
+ context_color: rgb::Color,
539
+ glyphs: &[impl Glyph],
540
+ text: &str,
541
+ ) {
542
+ let font_name = self
543
+ .rd_builder
544
+ .register_resource(sc.register_font_identifier(font_identifier));
545
+ self.content.set_font(font_name.to_pdf_name(), size);
546
+ self.content.set_text_matrix(
547
+ Transform::from_row(1.0, 0.0, 0.0, -1.0, *cur_x, cur_y).to_pdf_transform(),
548
+ );
549
+
550
+ for glyph in glyphs {
551
+ if glyph.glyph_id() == GlyphId::new(0)
552
+ || pdf_font.font().postscript_name() == Some("LastResort")
553
+ {
554
+ sc.register_validation_error(ValidationError::ContainsNotDefGlyph(
555
+ pdf_font.font(),
556
+ glyph.location(),
557
+ text[glyph.text_range()].to_string(),
558
+ ));
559
+ }
560
+ }
561
+
562
+ // If it's just a single glyph and without an x_offset, there is no need for individual
563
+ // positioning, so we can use `Tj` instead of `TJ`. The reason we're doing this is because
564
+ // Acrobat's copy-paste is a bit buggy if `/ActualText` is used at the end of a line, but
565
+ // only if `TJ` is used. With `Tj`, it's fine.
566
+ if let [glyph] = glyphs {
567
+ if approx_eq!(f32, glyph.x_offset(1.0), 0.0, epsilon = 0.001) {
568
+ self.encode_single_glyph(cur_x, pdf_font, size, context_color, glyph);
569
+ return;
570
+ }
571
+ }
572
+
573
+ self.encode_glyphs_with_individual_positioning(
574
+ cur_x,
575
+ pdf_font,
576
+ size,
577
+ context_color,
578
+ glyphs,
579
+ );
580
+ }
581
+
582
+ fn encode_single_glyph(
583
+ &mut self,
584
+ cur_x: &mut f32,
585
+ pdf_font: &dyn PdfFont,
586
+ size: f32,
587
+ context_color: rgb::Color,
588
+ glyph: &impl Glyph,
589
+ ) {
590
+ let pdf_glyph = pdf_font
591
+ .get_gid(ColoredGlyph::new(glyph.glyph_id(), context_color))
592
+ .unwrap();
593
+ self.scratch.clear();
594
+ let encoded = &mut self.scratch;
595
+ pdf_glyph.encode_into(encoded);
596
+ self.content.show(Str(encoded));
597
+ *cur_x += glyph.x_advance(size);
598
+ }
599
+
600
+ fn encode_glyphs_with_individual_positioning(
601
+ &mut self,
602
+ cur_x: &mut f32,
603
+ pdf_font: &dyn PdfFont,
604
+ size: f32,
605
+ context_color: rgb::Color,
606
+ glyphs: &[impl Glyph],
607
+ ) {
608
+ self.scratch.clear();
609
+ let encoded = &mut self.scratch;
610
+
611
+ let mut adjustment = 0.0;
612
+ let mut positioned = self.content.show_positioned();
613
+ let mut items = positioned.items();
614
+
615
+ for glyph in glyphs {
616
+ let pdf_glyph = pdf_font
617
+ .get_gid(ColoredGlyph::new(glyph.glyph_id(), context_color))
618
+ .unwrap();
619
+
620
+ let scale = |val| val * pdf_font.units_per_em();
621
+
622
+ let x_advance = scale(glyph.x_advance(1.0));
623
+ let font_advance = pdf_font
624
+ .font()
625
+ .advance_width(glyph.glyph_id())
626
+ .map(|n| scale(n / pdf_font.font().units_per_em()));
627
+ let x_offset = scale(glyph.x_offset(1.0));
628
+
629
+ adjustment += x_offset;
630
+
631
+ // Make sure we don't write miniscule adjustments
632
+ if !approx_eq!(f32, adjustment, 0.0, epsilon = 0.001) {
633
+ if !encoded.is_empty() {
634
+ items.show(Str(encoded));
635
+ encoded.clear();
636
+ }
637
+
638
+ // Adjustment is always in 1000 units, even for Type3 fonts.
639
+ items.adjust(-(adjustment / pdf_font.units_per_em() * PDF_UNITS_PER_EM));
640
+ adjustment = 0.0;
641
+ }
642
+
643
+ pdf_glyph.encode_into(encoded);
644
+
645
+ if let Some(font_advance) = font_advance {
646
+ adjustment += x_advance - font_advance;
647
+ }
648
+
649
+ adjustment -= x_offset;
650
+ // cur_x/cur_y and glyph metrics are in user space units.
651
+ *cur_x += glyph.x_advance(size);
652
+ }
653
+
654
+ if !encoded.is_empty() {
655
+ items.show(Str(encoded));
656
+ }
657
+
658
+ items.finish();
659
+ positioned.finish();
660
+ }
661
+
662
+ #[allow(clippy::too_many_arguments)]
663
+ fn fill_stroke_glyph_run(
664
+ &mut self,
665
+ x: f32,
666
+ ys: f32,
667
+ sc: &mut SerializeContext,
668
+ chunk_container: &mut ChunkContainer,
669
+ fill_render_mode: TextRenderingMode,
670
+ action: impl FnOnce(&mut ContentBuilder, &mut SerializeContext, &mut ChunkContainer),
671
+ glyphs: &[impl Glyph],
672
+ font: Font,
673
+ context_color: rgb::Color,
674
+ text: &str,
675
+ font_size: f32,
676
+ ) {
677
+ if glyphs.is_empty() {
678
+ return;
679
+ }
680
+
681
+ self.apply_isolated_op(
682
+ |_, _, _| {},
683
+ |sb, sc, chunk_container| {
684
+ let mut cur_x = x;
685
+ let mut cur_y = ys;
686
+
687
+ // When the glyph ranges are monotonically decreasing, the glyphs are in reverse
688
+ // order and we should write `/ReversedChars` marked content.
689
+ let reversed = glyphs.len() > 1
690
+ && glyphs
691
+ .windows(2)
692
+ .all(|w| w[0].text_range().start >= w[1].text_range().end);
693
+
694
+ action(sb, sc, chunk_container);
695
+ sb.content.begin_text();
696
+
697
+ let font_container = sc.register_font_container(font.clone());
698
+ let do_text_span = reversed
699
+ || use_text_spanner(
700
+ glyphs,
701
+ text,
702
+ context_color,
703
+ &mut font_container.borrow_mut(),
704
+ );
705
+
706
+ if do_text_span {
707
+ // Separate into distinct glyph runs that either are encoded using actual text, or are
708
+ // not.
709
+ let spanned = GlyphSpanner::new(
710
+ glyphs,
711
+ text,
712
+ reversed,
713
+ sc.serialize_settings()
714
+ .validators()
715
+ .requires_codepoint_mappings(),
716
+ context_color,
717
+ font_container.clone(),
718
+ );
719
+
720
+ for fragment in spanned {
721
+ sb.fill_stroke_glyph_span(
722
+ &mut cur_x,
723
+ &mut cur_y,
724
+ fragment,
725
+ sc,
726
+ fill_render_mode,
727
+ font_container.clone(),
728
+ context_color,
729
+ text,
730
+ font_size,
731
+ reversed,
732
+ )
733
+ }
734
+ } else {
735
+ let glyph_span = GlyphSpan::Unspanned(glyphs);
736
+
737
+ sb.fill_stroke_glyph_span(
738
+ &mut cur_x,
739
+ &mut cur_y,
740
+ glyph_span,
741
+ sc,
742
+ fill_render_mode,
743
+ font_container.clone(),
744
+ context_color,
745
+ text,
746
+ font_size,
747
+ reversed,
748
+ )
749
+ }
750
+
751
+ sb.content.end_text();
752
+ },
753
+ sc,
754
+ chunk_container,
755
+ )
756
+ }
757
+
758
+ #[allow(clippy::too_many_arguments)]
759
+ fn fill_stroke_glyph_span(
760
+ &mut self,
761
+ cur_x: &mut f32,
762
+ cur_y: &mut f32,
763
+ fragment: GlyphSpan<'_, impl Glyph>,
764
+ sc: &mut SerializeContext,
765
+ fill_render_mode: TextRenderingMode,
766
+ font_container: Rc<RefCell<FontContainer>>,
767
+ context_color: rgb::Color,
768
+ text: &str,
769
+ font_size: f32,
770
+ reversed: bool,
771
+ ) {
772
+ if let Some(text) = fragment.actual_text() {
773
+ let mut actual_text = self
774
+ .content
775
+ .begin_marked_content_with_properties(Name(b"Span"));
776
+ actual_text.properties().actual_text(TextStr(text));
777
+ }
778
+
779
+ if reversed {
780
+ self.content.begin_marked_content(Name(b"ReversedChars"));
781
+ }
782
+
783
+ // Segment into glyph runs that can be encoded in one go using a PDF
784
+ // text showing operator (i.e. no y shift, same Type3 font, etc.)
785
+ let segmented = GlyphGrouper::new(font_container.clone(), context_color, fragment.glyphs());
786
+
787
+ for glyph_group in segmented {
788
+ self.fill_stroke_glyph_group(
789
+ cur_x,
790
+ cur_y,
791
+ glyph_group,
792
+ sc,
793
+ fill_render_mode,
794
+ font_container.clone(),
795
+ context_color,
796
+ text,
797
+ font_size,
798
+ )
799
+ }
800
+
801
+ if reversed {
802
+ self.content.end_marked_content();
803
+ }
804
+
805
+ if fragment.actual_text().is_some() {
806
+ self.content.end_marked_content();
807
+ }
808
+ }
809
+
810
+ #[allow(clippy::too_many_arguments)]
811
+ fn fill_stroke_glyph_group(
812
+ &mut self,
813
+ cur_x: &mut f32,
814
+ cur_y: &mut f32,
815
+ glyph_group: GlyphGroup<'_, impl Glyph>,
816
+ sc: &mut SerializeContext,
817
+ fill_render_mode: TextRenderingMode,
818
+ font_container: Rc<RefCell<FontContainer>>,
819
+ context_color: rgb::Color,
820
+ text: &str,
821
+ font_size: f32,
822
+ ) {
823
+ let borrowed = font_container.borrow();
824
+ let pdf_font = borrowed
825
+ .get_from_identifier(glyph_group.font_identifier.clone())
826
+ .unwrap();
827
+
828
+ if fill_render_mode == TextRenderingMode::Fill || pdf_font.force_fill() {
829
+ self.content
830
+ .set_text_rendering_mode(TextRenderingMode::Fill);
831
+ } else if fill_render_mode == TextRenderingMode::FillStroke {
832
+ self.content
833
+ .set_text_rendering_mode(TextRenderingMode::FillStroke);
834
+ } else {
835
+ self.content
836
+ .set_text_rendering_mode(TextRenderingMode::Stroke);
837
+ }
838
+
839
+ self.encode_consecutive_glyph_run(
840
+ sc,
841
+ cur_x,
842
+ *cur_y - glyph_group.y_offset * font_size,
843
+ glyph_group.font_identifier,
844
+ pdf_font,
845
+ font_size,
846
+ context_color,
847
+ glyph_group.glyphs,
848
+ text,
849
+ );
850
+
851
+ *cur_y -= glyph_group.y_advance * font_size;
852
+ }
853
+
854
+ pub(crate) fn draw_xobject(
855
+ &mut self,
856
+ sc: &mut SerializeContext,
857
+ chunk_container: &mut ChunkContainer,
858
+ x_object: XObject,
859
+ state: &ExtGState,
860
+ ) {
861
+ let bbox = x_object.bbox();
862
+ self.apply_isolated_op(
863
+ |sb, _, _| {
864
+ sb.graphics_states.combine(state);
865
+ sb.expand_bbox(bbox);
866
+ },
867
+ move |sb, sc, chunk_container| {
868
+ let x_object_name = sb
869
+ .rd_builder
870
+ .register_resource(sc.register_resourceable(chunk_container, x_object));
871
+ sb.content.x_object(x_object_name.to_pdf_name());
872
+ },
873
+ sc,
874
+ chunk_container,
875
+ );
876
+ }
877
+
878
+ #[allow(dead_code)]
879
+ pub(crate) fn draw_xobject_by_reference(
880
+ &mut self,
881
+ sc: &mut SerializeContext,
882
+ chunk_container: &mut ChunkContainer,
883
+ bbox: Rect,
884
+ x_object: Ref,
885
+ ) {
886
+ // TODO: Consider bbox of XObject somehow?
887
+ self.apply_isolated_op(
888
+ |sb, _, _| {
889
+ sb.expand_bbox(bbox);
890
+ },
891
+ move |sb, _, _| {
892
+ let x_object_name = sb
893
+ .rd_builder
894
+ .register_resource(resource::XObject::new(x_object));
895
+ sb.content.x_object(x_object_name.to_pdf_name());
896
+ },
897
+ sc,
898
+ chunk_container,
899
+ );
900
+ }
901
+
902
+ pub(crate) fn draw_masked(
903
+ &mut self,
904
+ sc: &mut SerializeContext,
905
+ chunk_container: &mut ChunkContainer,
906
+ mask: Mask,
907
+ stream: Stream,
908
+ ) {
909
+ let state = ExtGState::new().mask(mask, sc, chunk_container);
910
+ self.uses_mask = true;
911
+ let x_object = XObject::new(stream, false, true, None);
912
+ self.draw_xobject(sc, chunk_container, x_object, &state);
913
+ }
914
+
915
+ pub(crate) fn draw_opacified(
916
+ &mut self,
917
+ sc: &mut SerializeContext,
918
+ chunk_container: &mut ChunkContainer,
919
+ opacity: NormalizedF32,
920
+ stream: Stream,
921
+ ) {
922
+ let state = ExtGState::new()
923
+ .stroking_alpha(opacity)
924
+ .non_stroking_alpha(opacity);
925
+ let x_object = XObject::new(stream, true, false, None);
926
+ self.draw_xobject(sc, chunk_container, x_object, &state);
927
+ }
928
+
929
+ pub(crate) fn draw_isolated(
930
+ &mut self,
931
+ sc: &mut SerializeContext,
932
+ chunk_container: &mut ChunkContainer,
933
+ stream: Stream,
934
+ ) {
935
+ let state = ExtGState::new();
936
+ let x_object = XObject::new(stream, true, false, None);
937
+ self.draw_xobject(sc, chunk_container, x_object, &state);
938
+ }
939
+
940
+ #[cfg(feature = "raster-images")]
941
+ pub(crate) fn draw_image(
942
+ &mut self,
943
+ image: Image,
944
+ size: Size,
945
+ sc: &mut SerializeContext,
946
+ chunk_container: &mut ChunkContainer,
947
+ ) {
948
+ self.apply_isolated_op(
949
+ |sb, _, _| {
950
+ // Scale the image from 1x1 to the actual dimensions.
951
+ let transform =
952
+ Transform::from_row(size.width(), 0.0, 0.0, -size.height(), 0.0, size.height());
953
+ sb.concat_transform(&transform);
954
+ sb.expand_bbox(Rect::from_xywh(0.0, 0.0, 1.0, 1.0).unwrap());
955
+ },
956
+ move |sb, sc, chunk_container| {
957
+ let image_name = sb.rd_builder.register_resource(resource::XObject::new(
958
+ sc.register_image(chunk_container, image),
959
+ ));
960
+
961
+ sb.content.x_object(image_name.to_pdf_name());
962
+ },
963
+ sc,
964
+ chunk_container,
965
+ );
966
+ }
967
+
968
+ pub(crate) fn draw_shading(
969
+ &mut self,
970
+ shading: &ShadingFunction,
971
+ sc: &mut SerializeContext,
972
+ chunk_container: &mut ChunkContainer,
973
+ ) {
974
+ self.apply_isolated_op(
975
+ |_, _, _| {},
976
+ move |sb, sc, chunk_container| {
977
+ let sh = sb
978
+ .rd_builder
979
+ .register_resource(sc.register_resourceable(chunk_container, shading.clone()));
980
+ sb.content.shading(sh.to_pdf_name());
981
+ },
982
+ sc,
983
+ chunk_container,
984
+ )
985
+ }
986
+
987
+ fn set_fill_opacity(&mut self, alpha: NormalizedF32) {
988
+ if alpha.get() != 1.0 {
989
+ let state = ExtGState::new().non_stroking_alpha(alpha);
990
+ self.graphics_states.combine(&state);
991
+ }
992
+ }
993
+
994
+ fn set_stroke_opacity(&mut self, alpha: NormalizedF32) {
995
+ if alpha.get() != 1.0 {
996
+ let state = ExtGState::new().stroking_alpha(alpha);
997
+ self.graphics_states.combine(&state);
998
+ }
999
+ }
1000
+
1001
+ fn apply_isolated_op(
1002
+ &mut self,
1003
+ prep: impl FnOnce(&mut Self, &mut SerializeContext, &mut ChunkContainer),
1004
+ op: impl FnOnce(&mut Self, &mut SerializeContext, &mut ChunkContainer),
1005
+ sc: &mut SerializeContext,
1006
+ chunk_container: &mut ChunkContainer,
1007
+ ) {
1008
+ self.save_graphics_state();
1009
+ self.content_save_state();
1010
+
1011
+ prep(self, sc, chunk_container);
1012
+
1013
+ let transform = self.cur_transform_with_root_transform();
1014
+
1015
+ if transform != Transform::identity() {
1016
+ self.content.transform(transform.to_pdf_transform());
1017
+ }
1018
+
1019
+ let state = self.graphics_states.cur().ext_g_state().clone();
1020
+
1021
+ if !state.empty() {
1022
+ let ext = self.rd_builder.register_resource::<resource::ExtGState>(
1023
+ sc.register_resourceable(chunk_container, state),
1024
+ );
1025
+ self.content.set_parameters(ext.to_pdf_name());
1026
+ }
1027
+
1028
+ op(self, sc, chunk_container);
1029
+
1030
+ self.content.restore_state();
1031
+ self.restore_graphics_state();
1032
+ }
1033
+
1034
+ #[allow(clippy::too_many_arguments)]
1035
+ fn content_set_fill_stroke_properties(
1036
+ &mut self,
1037
+ bounds: Rect,
1038
+ paint: &Paint,
1039
+ opacity: NormalizedF32,
1040
+ sc: &mut SerializeContext,
1041
+ chunk_container: &mut ChunkContainer,
1042
+ mut set_pattern_fn: impl FnMut(&mut Content, String),
1043
+ mut set_solid_fn: impl FnMut(&mut Content, ContentColorSpace, &Color),
1044
+ ) {
1045
+ let pattern_transform = |transform: Transform| -> Transform {
1046
+ transform.post_concat(self.cur_transform_with_root_transform())
1047
+ };
1048
+
1049
+ let mut write_gradient =
1050
+ |gradient_props: GradientProperties,
1051
+ sc: &mut SerializeContext,
1052
+ chunk_container: &mut ChunkContainer,
1053
+ transform: Transform,
1054
+ content_builder: &mut ContentBuilder| {
1055
+ if let Some((color, opacity)) = gradient_props.single_stop_color() {
1056
+ // Write gradients with one stop as a solid color fill.
1057
+ content_builder.set_fill_opacity(opacity);
1058
+ let cs = color.color_space(sc);
1059
+ let color_space_resource =
1060
+ Self::cs_to_content_cs(content_builder, sc, chunk_container, cs);
1061
+ set_solid_fn(&mut content_builder.content, color_space_resource, color);
1062
+ } else {
1063
+ let shading_mask = Mask::new_from_shading(
1064
+ gradient_props.clone(),
1065
+ transform,
1066
+ bounds,
1067
+ sc,
1068
+ chunk_container,
1069
+ );
1070
+
1071
+ let shading_pattern = ShadingPattern::new(
1072
+ gradient_props,
1073
+ content_builder
1074
+ .cur_transform_with_root_transform()
1075
+ .pre_concat(transform),
1076
+ );
1077
+ let color_space = content_builder
1078
+ .rd_builder
1079
+ .register_resource::<resource::Pattern>(
1080
+ sc.register_resourceable(chunk_container, shading_pattern),
1081
+ );
1082
+
1083
+ if let Some(shading_mask) = shading_mask {
1084
+ let state = ExtGState::new().mask(shading_mask, sc, chunk_container);
1085
+ content_builder.uses_mask = true;
1086
+
1087
+ let ext = content_builder
1088
+ .rd_builder
1089
+ .register_resource::<resource::ExtGState>(
1090
+ sc.register_resourceable(chunk_container, state),
1091
+ );
1092
+ content_builder.content.set_parameters(ext.to_pdf_name());
1093
+ }
1094
+
1095
+ set_pattern_fn(&mut content_builder.content, color_space);
1096
+ }
1097
+ };
1098
+
1099
+ match &paint.0 {
1100
+ InnerPaint::Color(c) => {
1101
+ let cs = c.color_space(sc);
1102
+ let color_space_resource = Self::cs_to_content_cs(self, sc, chunk_container, cs);
1103
+ set_solid_fn(&mut self.content, color_space_resource, c);
1104
+ }
1105
+ InnerPaint::LinearGradient(lg) => {
1106
+ let (gradient_props, transform) = lg.clone().gradient_properties(bounds);
1107
+ write_gradient(gradient_props, sc, chunk_container, transform, self);
1108
+ }
1109
+ InnerPaint::RadialGradient(rg) => {
1110
+ let (gradient_props, transform) = rg.clone().gradient_properties(bounds);
1111
+ write_gradient(gradient_props, sc, chunk_container, transform, self);
1112
+ }
1113
+ InnerPaint::SweepGradient(sg) => {
1114
+ let (gradient_props, transform) = sg.clone().gradient_properties(bounds);
1115
+ write_gradient(gradient_props, sc, chunk_container, transform, self);
1116
+ }
1117
+ InnerPaint::Pattern(pat) => {
1118
+ let mut pat = Arc::unwrap_or_clone(pat.clone());
1119
+ pat.transform = pattern_transform(pat.transform);
1120
+
1121
+ let tiling_pattern = TilingPattern::new(
1122
+ pat.stream,
1123
+ pat.transform,
1124
+ opacity,
1125
+ pat.width,
1126
+ pat.height,
1127
+ sc,
1128
+ chunk_container,
1129
+ );
1130
+
1131
+ let color_space = self.rd_builder.register_resource::<resource::Pattern>(
1132
+ sc.register_resourceable(chunk_container, tiling_pattern),
1133
+ );
1134
+ set_pattern_fn(&mut self.content, color_space);
1135
+ }
1136
+ }
1137
+ }
1138
+
1139
+ fn cs_to_content_cs(
1140
+ content_builder: &mut ContentBuilder,
1141
+ sc: &mut SerializeContext,
1142
+ chunk_container: &mut ChunkContainer,
1143
+ cs: ColorSpace,
1144
+ ) -> ContentColorSpace {
1145
+ match sc.register_colorspace(chunk_container, cs) {
1146
+ MaybeDeviceColorSpace::ColorSpace(s) => {
1147
+ ContentColorSpace::Named(content_builder.rd_builder.register_resource(s))
1148
+ }
1149
+ MaybeDeviceColorSpace::DeviceGray => ContentColorSpace::Device,
1150
+ MaybeDeviceColorSpace::DeviceRgb => ContentColorSpace::Device,
1151
+ MaybeDeviceColorSpace::DeviceCMYK => ContentColorSpace::Device,
1152
+ }
1153
+ }
1154
+
1155
+ fn content_set_fill_properties(
1156
+ &mut self,
1157
+ bounds: Rect,
1158
+ fill: &Fill,
1159
+ serializer_context: &mut SerializeContext,
1160
+ chunk_container: &mut ChunkContainer,
1161
+ ) {
1162
+ fn set_pattern_fn(content: &mut Content, color_space: String) {
1163
+ content.set_fill_color_space(pdf_writer::types::ColorSpaceOperand::Pattern);
1164
+ content.set_fill_pattern(None, color_space.to_pdf_name());
1165
+ }
1166
+
1167
+ fn set_solid_fn(content: &mut Content, color_space: ContentColorSpace, color: &Color) {
1168
+ match color_space {
1169
+ ContentColorSpace::Device => match color {
1170
+ Color::Regular(crate::color::RegularColor::Rgb(r)) => {
1171
+ let comps = r.to_pdf_color();
1172
+ content.set_fill_rgb(comps[0], comps[1], comps[2]);
1173
+ }
1174
+ Color::Regular(crate::color::RegularColor::Luma(l)) => {
1175
+ content.set_fill_gray(l.to_pdf_color());
1176
+ }
1177
+ Color::Regular(crate::color::RegularColor::Cmyk(c)) => {
1178
+ let comps = c.to_pdf_color();
1179
+ content.set_fill_cmyk(comps[0], comps[1], comps[2], comps[3]);
1180
+ }
1181
+ Color::Special(_) => {
1182
+ panic!("Device color space cannot be used with special colors")
1183
+ }
1184
+ },
1185
+ ContentColorSpace::Named(n) => {
1186
+ content.set_fill_color_space(n.to_pdf_name());
1187
+ content.set_fill_color(color.to_pdf_color());
1188
+ }
1189
+ }
1190
+ }
1191
+
1192
+ self.content_set_fill_stroke_properties(
1193
+ bounds,
1194
+ &fill.paint,
1195
+ fill.opacity,
1196
+ serializer_context,
1197
+ chunk_container,
1198
+ set_pattern_fn,
1199
+ set_solid_fn,
1200
+ );
1201
+ }
1202
+
1203
+ fn content_set_stroke_properties(
1204
+ &mut self,
1205
+ bounds: Rect,
1206
+ stroke: &Stroke,
1207
+ serializer_context: &mut SerializeContext,
1208
+ chunk_container: &mut ChunkContainer,
1209
+ ) {
1210
+ fn set_pattern_fn(content: &mut Content, color_space: String) {
1211
+ content.set_stroke_color_space(pdf_writer::types::ColorSpaceOperand::Pattern);
1212
+ content.set_stroke_pattern(None, color_space.to_pdf_name());
1213
+ }
1214
+
1215
+ fn set_solid_fn(content: &mut Content, color_space: ContentColorSpace, color: &Color) {
1216
+ match color_space {
1217
+ ContentColorSpace::Device => match color {
1218
+ Color::Regular(crate::color::RegularColor::Rgb(r)) => {
1219
+ let comps = r.to_pdf_color();
1220
+ content.set_stroke_rgb(comps[0], comps[1], comps[2]);
1221
+ }
1222
+ Color::Regular(crate::color::RegularColor::Luma(l)) => {
1223
+ content.set_stroke_gray(l.to_pdf_color());
1224
+ }
1225
+ Color::Regular(crate::color::RegularColor::Cmyk(c)) => {
1226
+ let comps = c.to_pdf_color();
1227
+ content.set_stroke_cmyk(comps[0], comps[1], comps[2], comps[3]);
1228
+ }
1229
+ Color::Special(_) => {
1230
+ panic!("Device color space cannot be used with special colors")
1231
+ }
1232
+ },
1233
+ ContentColorSpace::Named(n) => {
1234
+ content.set_stroke_color_space(n.to_pdf_name());
1235
+ content.set_stroke_color(color.to_pdf_color());
1236
+ }
1237
+ }
1238
+ }
1239
+
1240
+ self.content_set_fill_stroke_properties(
1241
+ bounds,
1242
+ &stroke.paint,
1243
+ stroke.opacity,
1244
+ serializer_context,
1245
+ chunk_container,
1246
+ set_pattern_fn,
1247
+ set_solid_fn,
1248
+ );
1249
+
1250
+ // Only write if they don't correspond to the default values as defined in the
1251
+ // PDF specification.
1252
+ if stroke.width != 1.0 {
1253
+ self.content.set_line_width(stroke.width);
1254
+ }
1255
+
1256
+ if stroke.miter_limit != 10.0 {
1257
+ self.content.set_miter_limit(stroke.miter_limit);
1258
+ }
1259
+
1260
+ if stroke.line_cap != LineCap::Butt {
1261
+ self.content.set_line_cap(stroke.line_cap.to_pdf_line_cap());
1262
+ }
1263
+
1264
+ if stroke.line_join != LineJoin::Miter {
1265
+ self.content
1266
+ .set_line_join(stroke.line_join.to_pdf_line_join());
1267
+ }
1268
+
1269
+ if let Some(stroke_dash) = &stroke.dash {
1270
+ self.content
1271
+ .set_dash_pattern(stroke_dash.array.iter().copied(), stroke_dash.offset);
1272
+ }
1273
+ }
1274
+
1275
+ fn content_draw_path(&mut self, path_data: impl Iterator<Item = PathSegment>) {
1276
+ // Taken from resvg
1277
+ fn calc(n1: f32, n2: f32) -> f32 {
1278
+ (n1 + n2 * 2.0) / 3.0
1279
+ }
1280
+
1281
+ let mut p_prev = None;
1282
+
1283
+ for operation in path_data {
1284
+ match operation {
1285
+ PathSegment::MoveTo(p) => {
1286
+ self.content.move_to(p.x, p.y);
1287
+ p_prev = Some(p);
1288
+ }
1289
+ PathSegment::LineTo(p) => {
1290
+ self.content.line_to(p.x, p.y);
1291
+ p_prev = Some(p);
1292
+ }
1293
+ PathSegment::QuadTo(p1, p2) => {
1294
+ // Since PDF doesn't support quad curves, we need to convert them into
1295
+ // cubic.
1296
+ let prev = p_prev.unwrap();
1297
+ self.content.cubic_to(
1298
+ calc(prev.x, p1.x),
1299
+ calc(prev.y, p1.y),
1300
+ calc(p2.x, p1.x),
1301
+ calc(p2.y, p1.y),
1302
+ p2.x,
1303
+ p2.y,
1304
+ );
1305
+ p_prev = Some(p2);
1306
+ }
1307
+ PathSegment::CubicTo(p1, p2, p3) => {
1308
+ self.content.cubic_to(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y);
1309
+ p_prev = Some(p3);
1310
+ }
1311
+ PathSegment::Close => {
1312
+ self.content.close_path();
1313
+ }
1314
+ };
1315
+ }
1316
+ }
1317
+ }
1318
+
1319
+ // Note that this isn't a 100% accurate calculation, it can overestimate (and in a few cases
1320
+ // even underestimate), but it should be good enough for the majority of the cases.
1321
+ // TODO: Improve this so that `zalgo_text` test case shows up fully in the reference image.
1322
+ fn get_glyphs_bbox(glyphs: &[impl Glyph], x: f32, y: f32, size: f32, font: Font) -> Rect {
1323
+ let font_bbox = font.bbox();
1324
+ let (mut bl, mut bt, mut br, mut bb) = font_bbox
1325
+ .transform(Transform::from_scale(
1326
+ size / font.units_per_em(),
1327
+ -size / font.units_per_em(),
1328
+ ))
1329
+ .and_then(|b| b.transform(Transform::from_translate(x, y)))
1330
+ .map(|b| (b.left(), b.top(), b.right(), b.bottom()))
1331
+ .unwrap_or((x, y, x + 1.0, y + 1.0));
1332
+
1333
+ let mut x = x;
1334
+ let mut y = y;
1335
+
1336
+ for glyph in glyphs {
1337
+ let xo = glyph.x_offset(size);
1338
+ let xa = glyph.x_advance(size);
1339
+ let yo = glyph.y_offset(size);
1340
+ let ya = glyph.y_advance(size);
1341
+
1342
+ x += xa;
1343
+ y -= ya;
1344
+
1345
+ bl = bl.min(x + xo);
1346
+ br = br.max(x + xo);
1347
+ bt = bt.min(y - yo);
1348
+ bb = bb.max(y - yo);
1349
+ }
1350
+
1351
+ Rect::from_ltrb(bl, bt, br, bb).unwrap()
1352
+ }