1#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
15#![forbid(unsafe_code)]
16
17mod metrics;
18mod symbol;
19
20pub use symbol::{symbol_code, symbol_width};
21
22use std::fmt::Write as _;
23
24pub type Rgb = [u8; 3];
26
27#[derive(Clone, Copy, Debug, Default, PartialEq)]
29pub struct Point {
30 pub x: f32,
31 pub y: f32,
32}
33
34impl Point {
35 pub const fn new(x: f32, y: f32) -> Self {
36 Self { x, y }
37 }
38}
39
40#[derive(Clone, Copy, Debug, Default, PartialEq)]
42pub struct Rect {
43 pub x: f32,
44 pub y: f32,
45 pub width: f32,
46 pub height: f32,
47}
48
49impl Rect {
50 pub const fn new(x: f32, y: f32, width: f32, height: f32) -> Self {
51 Self { x, y, width, height }
52 }
53}
54
55#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
57pub enum Font {
58 #[default]
59 Helvetica,
60 HelveticaBold,
61 HelveticaOblique,
62 HelveticaBoldOblique,
63 TimesRoman,
64 TimesBold,
65 TimesItalic,
66 TimesBoldItalic,
67 Courier,
68 CourierBold,
69 CourierOblique,
70 CourierBoldOblique,
71 Symbol,
73}
74
75const FONT_COUNT: usize = 13;
77
78#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
80pub enum Family {
81 #[default]
82 Sans,
83 Serif,
84 Mono,
85}
86
87impl Font {
88 pub const ALL: [Font; FONT_COUNT] = [
89 Font::Helvetica,
90 Font::HelveticaBold,
91 Font::HelveticaOblique,
92 Font::HelveticaBoldOblique,
93 Font::TimesRoman,
94 Font::TimesBold,
95 Font::TimesItalic,
96 Font::TimesBoldItalic,
97 Font::Courier,
98 Font::CourierBold,
99 Font::CourierOblique,
100 Font::CourierBoldOblique,
101 Font::Symbol,
102 ];
103
104 pub fn base_name(self) -> &'static str {
106 match self {
107 Font::Helvetica => "Helvetica",
108 Font::HelveticaBold => "Helvetica-Bold",
109 Font::HelveticaOblique => "Helvetica-Oblique",
110 Font::HelveticaBoldOblique => "Helvetica-BoldOblique",
111 Font::TimesRoman => "Times-Roman",
112 Font::TimesBold => "Times-Bold",
113 Font::TimesItalic => "Times-Italic",
114 Font::TimesBoldItalic => "Times-BoldItalic",
115 Font::Courier => "Courier",
116 Font::CourierBold => "Courier-Bold",
117 Font::CourierOblique => "Courier-Oblique",
118 Font::CourierBoldOblique => "Courier-BoldOblique",
119 Font::Symbol => "Symbol",
120 }
121 }
122
123 fn index(self) -> usize {
124 Font::ALL.iter().position(|f| *f == self).unwrap_or(0)
125 }
126
127 pub fn styled(family: Family, bold: bool, italic: bool) -> Font {
129 match (family, bold, italic) {
130 (Family::Sans, false, false) => Font::Helvetica,
131 (Family::Sans, true, false) => Font::HelveticaBold,
132 (Family::Sans, false, true) => Font::HelveticaOblique,
133 (Family::Sans, true, true) => Font::HelveticaBoldOblique,
134 (Family::Serif, false, false) => Font::TimesRoman,
135 (Family::Serif, true, false) => Font::TimesBold,
136 (Family::Serif, false, true) => Font::TimesItalic,
137 (Family::Serif, true, true) => Font::TimesBoldItalic,
138 (Family::Mono, false, false) => Font::Courier,
139 (Family::Mono, true, false) => Font::CourierBold,
140 (Family::Mono, false, true) => Font::CourierOblique,
141 (Family::Mono, true, true) => Font::CourierBoldOblique,
142 }
143 }
144
145 pub fn family_for(name: &str) -> Family {
148 let n = name.to_ascii_lowercase();
149 const SERIF: [&str; 10] = ["times", "cambria", "georgia", "garamond", "serif", "book", "palatino", "baskerville", "tinos", "caladea"];
150 const MONO: [&str; 8] = ["courier", "consolas", "mono", "menlo", "lucida console", "cousine", "code", "typewriter"];
151 if MONO.iter().any(|m| n.contains(m)) {
152 Family::Mono
153 } else if SERIF.iter().any(|s| n.contains(s)) && !n.contains("sans") {
154 Family::Serif
155 } else {
156 Family::Sans
157 }
158 }
159
160 pub fn can_encode(self, c: char) -> bool {
163 match self {
164 Font::Symbol => symbol_code(c).is_some(),
165 _ => c == '?' || win_ansi(c) != b'?',
166 }
167 }
168
169 pub fn char_width(self, c: char) -> u16 {
171 if self == Font::Symbol {
172 return symbol_code(c).and_then(symbol_width).unwrap_or(500);
173 }
174 let table: Option<&[u16; 95]> = match self {
175 Font::Helvetica | Font::HelveticaOblique => Some(&metrics::HELVETICA),
176 Font::HelveticaBold | Font::HelveticaBoldOblique => Some(&metrics::HELVETICA_BOLD),
177 Font::TimesRoman | Font::TimesItalic => Some(&metrics::TIMES),
178 Font::TimesBold | Font::TimesBoldItalic => Some(&metrics::TIMES_BOLD),
179 _ => None,
180 };
181 if needs_cid(c) {
182 return CID_WIDTH;
183 }
184 let Some(table) = table else { return 600 };
185 let ascii = |c: char| -> Option<u16> {
186 let i = (c as u32).checked_sub(32)? as usize;
187 table.get(i).copied()
188 };
189 if let Some(w) = ascii(c) {
190 return w;
191 }
192 match c {
193 '—' | '…' | '‰' | '™' => 1000,
194 _ => metrics::fold(c).and_then(ascii).unwrap_or(match self {
195 Font::TimesRoman | Font::TimesItalic | Font::TimesBold | Font::TimesBoldItalic => 500,
196 _ => 556,
197 }),
198 }
199 }
200}
201
202pub fn text_width(font: Font, size: f32, text: &str) -> f32 {
204 let units: u64 = text.chars().map(|c| u64::from(font.char_width(c))).sum();
206 units as f32 * size / 1000.0
207}
208
209pub fn win_ansi(c: char) -> u8 {
211 let u = c as u32;
212 if (0x20..0x7F).contains(&u) || (0xA0..=0xFF).contains(&u) {
213 return u as u8;
214 }
215 match c {
216 '€' => 0x80,
217 '‚' => 0x82,
218 'ƒ' => 0x83,
219 '„' => 0x84,
220 '…' => 0x85,
221 '†' => 0x86,
222 '‡' => 0x87,
223 'ˆ' => 0x88,
224 '‰' => 0x89,
225 'Š' => 0x8A,
226 '‹' => 0x8B,
227 'Œ' => 0x8C,
228 'Ž' => 0x8E,
229 '‘' => 0x91,
230 '’' => 0x92,
231 '“' => 0x93,
232 '”' => 0x94,
233 '•' => 0x95,
234 '–' => 0x96,
235 '—' => 0x97,
236 '˜' => 0x98,
237 '™' => 0x99,
238 'š' => 0x9A,
239 '›' => 0x9B,
240 'œ' => 0x9C,
241 'ž' => 0x9E,
242 'Ÿ' => 0x9F,
243 '\t' => b' ',
244 _ => b'?',
245 }
246}
247
248const CID_FONT: usize = Font::ALL.len();
250const FONT_SLOTS: usize = CID_FONT + 1;
252const CID_BASE_FONT: &str = "STSong-Light";
254const CID_WIDTH: u16 = 1000;
256
257fn needs_cid(c: char) -> bool {
259 !c.is_control() && c != '?' && win_ansi(c) == b'?'
260}
261
262fn pdf_utf16_string(text: &str) -> String {
264 let mut s = String::with_capacity(text.len() * 4 + 2);
265 s.push('<');
266 for unit in text.encode_utf16() {
267 let _ = write!(s, "{unit:04X}");
268 }
269 s.push('>');
270 s
271}
272
273fn pdf_string(text: &str) -> String {
275 pdf_string_in(text, false)
276}
277
278fn pdf_string_in(text: &str, symbol: bool) -> String {
281 let mut s = String::with_capacity(text.len() + 2);
282 s.push('(');
283 for c in text.chars() {
284 let b = if symbol { symbol_code(c).unwrap_or(b'?') } else { win_ansi(c) };
285 match b {
286 b'(' | b')' | b'\\' => {
287 s.push('\\');
288 s.push(b as char);
289 }
290 0x20..=0x7E => s.push(b as char),
291 _ => {
292 let _ = write!(s, "\\{b:03o}");
293 }
294 }
295 }
296 s.push(')');
297 s
298}
299
300fn pdf_uri_string(uri: &str) -> String {
302 let mut ascii = String::with_capacity(uri.len());
303 for byte in uri.bytes() {
304 if byte.is_ascii() {
305 ascii.push(byte as char);
306 } else {
307 let _ = write!(ascii, "%{byte:02X}");
308 }
309 }
310 pdf_string(&ascii)
311}
312
313fn n(v: f32) -> String {
315 let v = if v.is_finite() { v.clamp(-1.0e7, 1.0e7) } else { 0.0 };
316 let r = (v * 1000.0).round() / 1000.0;
317 if r == r.trunc() {
318 format!("{}", r as i64)
319 } else {
320 let s = format!("{r:.3}");
321 s.trim_end_matches('0').trim_end_matches('.').to_string()
322 }
323}
324
325fn finite(v: &[f32]) -> bool {
327 v.iter().all(|x| x.is_finite())
328}
329
330fn extent(v: f32) -> bool {
332 v > 0.0 && v.is_finite()
333}
334
335fn stroke_width(w: f32) -> f32 {
337 if w.is_finite() { w.max(0.0) } else { 0.0 }
338}
339
340fn dash_values(dash: &[f32]) -> String {
344 let d: Vec<f32> = dash.iter().take(8).map(|v| if v.is_finite() { v.max(0.0) } else { f32::NAN }).collect();
345 if !finite(&d) || !d.iter().any(|v| (v.min(1.0e7) * 1000.0).round() > 0.0) {
346 return String::new();
347 }
348 d.iter().map(|v| n(*v)).collect::<Vec<_>>().join(" ")
349}
350
351fn rgb_op(c: Rgb, stroke: bool) -> String {
352 let f = |v: u8| n(v as f32 / 255.0);
353 format!("{} {} {} {}", f(c[0]), f(c[1]), f(c[2]), if stroke { "RG" } else { "rg" })
354}
355
356struct ImageData {
357 width: u32,
358 height: u32,
359 rgb: Vec<u8>,
360 alpha: Option<Vec<u8>>,
361}
362
363enum LinkTarget {
364 Uri(String),
365 Page { index: usize, x: f32, y: f32 },
366}
367
368struct LinkAnnotation {
369 rect: [f32; 4],
371 target: LinkTarget,
372}
373
374pub struct Page {
376 width: f32,
377 height: f32,
378 content: String,
379 fonts: [bool; FONT_SLOTS],
380 images: Vec<ImageData>,
381 links: Vec<LinkAnnotation>,
382 depth: u32,
383}
384
385const MAX_PAGE_CONTENT: usize = 64 * 1024 * 1024;
388const MAX_IMAGE_PIXELS: u64 = 40_000_000;
390const MAX_PAGE_LINKS: usize = 4096;
391const MAX_LINK_URI_BYTES: usize = 4096;
392
393impl Page {
394 pub fn width(&self) -> f32 {
395 self.width
396 }
397 pub fn height(&self) -> f32 {
398 self.height
399 }
400 fn y(&self, y: f32) -> f32 {
401 self.height - y
402 }
403 fn full(&self) -> bool {
404 self.content.len() > MAX_PAGE_CONTENT
405 }
406 fn op(&mut self, s: &str) {
407 if !self.full() {
408 self.content.push_str(s);
409 self.content.push('\n');
410 }
411 }
412
413 fn link_rect(&self, rect: Rect) -> Option<[f32; 4]> {
414 let Rect { x, y, width, height } = rect;
415 if !finite(&[x, y, width, height]) || !extent(width) || !extent(height) || self.links.len() >= MAX_PAGE_LINKS {
416 return None;
417 }
418 let (right, bottom) = (x + width, y + height);
419 if !finite(&[right, bottom]) {
420 return None;
421 }
422 let (left, right) = (x.max(0.0), right.min(self.width));
423 let (top, bottom) = (y.max(0.0), bottom.min(self.height));
424 (right > left && bottom > top).then(|| [left, self.y(bottom), right, self.y(top)])
425 }
426
427 pub fn link_uri(&mut self, x: f32, y: f32, w: f32, h: f32, uri: &str) {
429 let allowed = uri
430 .split_once(':')
431 .is_some_and(|(scheme, rest)| matches!(scheme.to_ascii_lowercase().as_str(), "http" | "https" | "mailto") && !rest.is_empty());
432 if !allowed || uri.len() > MAX_LINK_URI_BYTES || uri.chars().any(char::is_control) {
433 return;
434 }
435 if let Some(rect) = self.link_rect(Rect::new(x, y, w, h)) {
436 self.links.push(LinkAnnotation { rect, target: LinkTarget::Uri(uri.to_string()) });
437 }
438 }
439
440 pub fn link_page(&mut self, rect: Rect, page: usize, target: Point) {
442 if !finite(&[target.x, target.y]) {
443 return;
444 }
445 if let Some(rect) = self.link_rect(rect) {
446 self.links.push(LinkAnnotation { rect, target: LinkTarget::Page { index: page, x: target.x, y: target.y } });
447 }
448 }
449
450 pub fn save(&mut self) {
452 self.depth = self.depth.saturating_add(1);
453 self.content.push_str("q\n");
454 }
455 pub fn restore(&mut self) {
457 if self.depth > 0 {
458 self.depth -= 1;
459 self.content.push_str("Q\n");
460 }
461 }
462 pub fn clip_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
464 let s = format!("{} {} {} {} re W n", n(x), n(self.y(y + h)), n(w.max(0.0)), n(h.max(0.0)));
465 self.op(&s);
466 }
467
468 pub fn fill_rect(&mut self, x: f32, y: f32, w: f32, h: f32, color: Rgb) {
469 if !(extent(w) && extent(h) && finite(&[x, y])) {
470 return;
471 }
472 let s = format!("{}\n{} {} {} {} re f", rgb_op(color, false), n(x), n(self.y(y + h)), n(w), n(h));
473 self.op(&s);
474 }
475
476 pub fn rect_stroke(&mut self, x: f32, y: f32, w: f32, h: f32, width: f32, color: Rgb) {
477 if !finite(&[x, y, w, h]) {
478 return;
479 }
480 let s = format!("{}\n{} w [] 0 d\n{} {} {} {} re S", rgb_op(color, true), n(stroke_width(width)), n(x), n(self.y(y + h)), n(w), n(h));
481 self.op(&s);
482 }
483
484 pub fn stroke_line(&mut self, x0: f32, y0: f32, x1: f32, y1: f32, width: f32, color: Rgb) {
485 self.stroke_line_dashed(Point::new(x0, y0), Point::new(x1, y1), width, color, &[]);
486 }
487
488 pub fn stroke_line_dashed(&mut self, start: Point, end: Point, width: f32, color: Rgb, dash: &[f32]) {
490 if !finite(&[start.x, start.y, end.x, end.y]) {
491 return;
492 }
493 let s = format!(
494 "{}\n{} w [{}] 0 d\n{} {} m {} {} l S",
495 rgb_op(color, true),
496 n(stroke_width(width)),
497 dash_values(dash),
498 n(start.x),
499 n(self.y(start.y)),
500 n(end.x),
501 n(self.y(end.y))
502 );
503 self.op(&s);
504 }
505
506 pub fn polyline(&mut self, pts: &[[f32; 2]], width: f32, color: Rgb, dash: &[f32]) {
508 if pts.len() < 2 || !pts.iter().all(|p| finite(p)) {
509 return;
510 }
511 let mut s = format!("{}\n{} w [{}] 0 d 1 j\n", rgb_op(color, true), n(stroke_width(width)), dash_values(dash));
512 self.path(&mut s, pts, false);
513 s.push_str(" S");
514 self.op(&s);
515 }
516
517 fn path(&self, s: &mut String, pts: &[[f32; 2]], close: bool) {
518 for (i, p) in pts.iter().enumerate() {
519 let _ = write!(s, "{} {} {} ", n(p[0]), n(self.y(p[1])), if i == 0 { "m" } else { "l" });
520 }
521 if close {
522 s.push('h');
523 }
524 }
525
526 pub fn polygon(&mut self, pts: &[[f32; 2]], fill: Option<Rgb>, stroke: Option<(Rgb, f32)>) {
528 if pts.len() < 2 || (fill.is_none() && stroke.is_none()) || !pts.iter().all(|p| finite(p)) {
529 return;
530 }
531 let mut s = String::new();
532 if let Some(f) = fill {
533 s.push_str(&rgb_op(f, false));
534 s.push('\n');
535 }
536 if let Some((c, w)) = stroke {
537 let _ = writeln!(s, "{}\n{} w [] 0 d 1 j", rgb_op(c, true), n(stroke_width(w)));
538 }
539 self.path(&mut s, pts, true);
540 s.push_str(match (fill.is_some(), stroke.is_some()) {
541 (true, true) => " B",
542 (true, false) => " f",
543 _ => " S",
544 });
545 self.op(&s);
546 }
547
548 pub fn circle(&mut self, cx: f32, cy: f32, r: f32, fill: Option<Rgb>, stroke: Option<(Rgb, f32)>) {
550 if !extent(r) || !finite(&[cx, cy]) || (fill.is_none() && stroke.is_none()) {
551 return;
552 }
553 const K: f32 = 0.552_284_8;
554 let k = r * K;
555 let y = self.y(cy);
556 let mut s = String::new();
557 if let Some(f) = fill {
558 s.push_str(&rgb_op(f, false));
559 s.push('\n');
560 }
561 if let Some((c, w)) = stroke {
562 let _ = writeln!(s, "{}\n{} w [] 0 d", rgb_op(c, true), n(stroke_width(w)));
563 }
564 let _ = write!(
565 s,
566 "{} {} m {} {} {} {} {} {} c {} {} {} {} {} {} c {} {} {} {} {} {} c {} {} {} {} {} {} c h",
567 n(cx + r),
568 n(y),
569 n(cx + r),
570 n(y + k),
571 n(cx + k),
572 n(y + r),
573 n(cx),
574 n(y + r),
575 n(cx - k),
576 n(y + r),
577 n(cx - r),
578 n(y + k),
579 n(cx - r),
580 n(y),
581 n(cx - r),
582 n(y - k),
583 n(cx - k),
584 n(y - r),
585 n(cx),
586 n(y - r),
587 n(cx + k),
588 n(y - r),
589 n(cx + r),
590 n(y - k),
591 n(cx + r),
592 n(y)
593 );
594 s.push_str(match (fill.is_some(), stroke.is_some()) {
595 (true, true) => " B",
596 (true, false) => " f",
597 _ => " S",
598 });
599 self.op(&s);
600 }
601
602 pub fn text(&mut self, x: f32, y: f32, size: f32, font: Font, color: Rgb, text: &str) {
604 self.text_rotated(Point::new(x, y), size, font, color, text, 0.0);
605 }
606
607 pub fn text_rotated(&mut self, origin: Point, size: f32, font: Font, color: Rgb, text: &str, angle: f32) {
610 if text.is_empty() || !extent(size) || !finite(&[origin.x, origin.y]) {
611 return;
612 }
613 let fi = font.index();
614 let size = n(size.min(10_000.0));
615 let (sn, cs) = if angle.is_finite() { angle.sin_cos() } else { (0.0, 1.0) };
616 let mut s = format!("BT\n{}\n{} {} {} {} {} {} Tm\n", rgb_op(color, false), n(cs), n(-sn), n(sn), n(cs), n(origin.x), n(self.y(origin.y)));
618 if font == Font::Symbol {
620 if let Some(f) = self.fonts.get_mut(fi) {
621 *f = true;
622 }
623 let _ = writeln!(s, "/F{} {} Tf\n{} Tj", fi + 1, size, pdf_string_in(text, true));
624 s.push_str("ET");
625 self.op(&s);
626 return;
627 }
628 let mut rest = text;
631 while let Some(first) = rest.chars().next() {
632 let cid = needs_cid(first);
633 let end = rest.char_indices().find(|(_, c)| needs_cid(*c) != cid).map_or(rest.len(), |(i, _)| i);
634 let (run, tail) = rest.split_at(end);
635 let (slot, string) = if cid { (CID_FONT, pdf_utf16_string(run)) } else { (fi, pdf_string(run)) };
636 if let Some(f) = self.fonts.get_mut(slot) {
637 *f = true;
638 }
639 let _ = writeln!(s, "/F{} {} Tf\n{} Tj", slot + 1, size, string);
640 rest = tail;
641 }
642 s.push_str("ET");
643 self.op(&s);
644 }
645
646 pub fn image_rgba(&mut self, destination: Rect, rgba: &[u8], pixel_size: [u32; 2]) {
649 let Rect { x, y, width: w, height: h } = destination;
650 let [pw, ph] = pixel_size;
651 let px = pw as u64 * ph as u64;
652 if pw == 0 || ph == 0 || px > MAX_IMAGE_PIXELS || (rgba.len() as u64) < px * 4 || !(extent(w) && extent(h) && finite(&[x, y])) {
653 return;
654 }
655 let px = px as usize;
656 let mut rgb = Vec::with_capacity(px * 3);
657 let mut alpha = Vec::with_capacity(px);
658 for p in rgba.as_chunks::<4>().0.iter().take(px) {
659 rgb.extend_from_slice(&[p[0], p[1], p[2]]);
660 alpha.push(p[3]);
661 }
662 let alpha = if alpha.iter().all(|a| *a == 255) { None } else { Some(alpha) };
663 let idx = self.images.len();
664 self.images.push(ImageData { width: pw, height: ph, rgb, alpha });
665 let s = format!("q {} 0 0 {} {} {} cm /Im{} Do Q", n(w), n(h), n(x), n(self.y(y + h)), idx + 1);
666 self.op(&s);
667 }
668}
669
670#[derive(Clone, Debug, Default)]
672pub struct Info {
673 pub title: String,
674 pub author: String,
675 pub creator: String,
676}
677
678#[derive(Default)]
680pub struct PdfDoc {
681 pages: Vec<Page>,
682 pub info: Info,
683}
684
685fn run_length(data: &[u8]) -> Vec<u8> {
687 let mut out = Vec::with_capacity(data.len() / 2 + 16);
688 let mut i = 0;
689 while i < data.len() {
690 let b = data[i];
691 let mut run = 1;
692 while i + run < data.len() && run < 128 && data[i + run] == b {
693 run += 1;
694 }
695 if run >= 2 {
696 out.push((257 - run) as u8);
697 out.push(b);
698 i += run;
699 continue;
700 }
701 let start = i;
703 let mut len = 0;
704 while i < data.len() && len < 128 {
705 if i + 2 < data.len() && data[i] == data[i + 1] && data[i] == data[i + 2] {
706 break;
707 }
708 i += 1;
709 len += 1;
710 }
711 if len == 0 {
712 continue;
714 }
715 out.push((len - 1) as u8);
716 out.extend_from_slice(&data[start..start + len]);
717 }
718 out.push(128);
719 out
720}
721
722const MAX_INFO_CHARS: usize = 1024;
724
725fn pdf_text_string(text: &str) -> String {
728 let text: String = text.chars().take(MAX_INFO_CHARS).collect();
729 if text.chars().all(|c| matches!(c, ' '..='~')) {
730 return pdf_string(&text);
731 }
732 let mut s = String::with_capacity(text.len() * 4 + 6);
733 s.push_str("<FEFF");
734 for unit in text.encode_utf16() {
735 let _ = write!(s, "{unit:04X}");
736 }
737 s.push('>');
738 s
739}
740
741impl PdfDoc {
742 pub fn new() -> PdfDoc {
743 PdfDoc::default()
744 }
745
746 pub fn add_page(&mut self, width_pt: f32, height_pt: f32) -> &mut Page {
748 let width = if width_pt.is_finite() { width_pt.clamp(3.0, 14_400.0) } else { 612.0 };
749 let height = if height_pt.is_finite() { height_pt.clamp(3.0, 14_400.0) } else { 792.0 };
750 self.pages.push(Page { width, height, content: String::new(), fonts: [false; FONT_SLOTS], images: Vec::new(), links: Vec::new(), depth: 0 });
751 let i = self.pages.len() - 1;
752 &mut self.pages[i]
753 }
754
755 pub fn page_count(&self) -> usize {
756 self.pages.len()
757 }
758
759 pub fn page_mut(&mut self, i: usize) -> Option<&mut Page> {
761 self.pages.get_mut(i)
762 }
763
764 pub fn finish(mut self) -> Vec<u8> {
766 if self.pages.is_empty() {
767 self.add_page(612.0, 792.0);
768 }
769 let mut used = [false; FONT_SLOTS];
771 for p in &self.pages {
772 for (u, f) in used.iter_mut().zip(p.fonts.iter()) {
773 *u |= *f;
774 }
775 }
776 let mut next = 4u32;
777 let mut font_ids = [0u32; FONT_SLOTS];
778 for (i, u) in used.iter().enumerate() {
779 if *u {
780 font_ids[i] = next;
781 next += if i == CID_FONT { 3 } else { 1 };
783 }
784 }
785 let mut objects: Vec<(u32, Vec<u8>)> = Vec::new();
786 let mut kids = Vec::new();
787 let mut annotations = Vec::new();
788 let page_count = self.pages.len();
789 let mut font_dict = String::new();
790 for (i, id) in font_ids.iter().enumerate() {
791 if *id != 0 {
792 let _ = write!(font_dict, "/F{} {} 0 R ", i + 1, id);
793 if i == CID_FONT {
794 let (cid_id, desc_id) = (id + 1, id + 2);
795 objects.push((
796 *id,
797 format!(
798 "<< /Type /Font /Subtype /Type0 /BaseFont /{CID_BASE_FONT} /Encoding /UniGB-UTF16-H /DescendantFonts [{cid_id} 0 R] >>"
799 )
800 .into_bytes(),
801 ));
802 objects.push((
803 cid_id,
804 format!(
805 "<< /Type /Font /Subtype /CIDFontType0 /BaseFont /{CID_BASE_FONT} /CIDSystemInfo << /Registry (Adobe) /Ordering (GB1) /Supplement 4 >> /FontDescriptor {desc_id} 0 R /DW {CID_WIDTH} >>"
806 )
807 .into_bytes(),
808 ));
809 objects.push((
810 desc_id,
811 format!(
812 "<< /Type /FontDescriptor /FontName /{CID_BASE_FONT} /Flags 6 /FontBBox [-25 -254 1000 880] /ItalicAngle 0 /Ascent 880 /Descent -120 /CapHeight 880 /StemV 93 >>"
813 )
814 .into_bytes(),
815 ));
816 continue;
817 }
818 let name = Font::ALL.get(i).copied().unwrap_or_default().base_name();
819 let enc = if name.starts_with("Symbol") { "" } else { " /Encoding /WinAnsiEncoding" };
820 objects.push((*id, format!("<< /Type /Font /Subtype /Type1 /BaseFont /{name}{enc} >>").into_bytes()));
821 }
822 }
823 for page in &mut self.pages {
824 while page.depth > 0 {
825 page.restore();
826 }
827 let mut xobjects = String::new();
828 for (k, img) in page.images.iter().enumerate() {
829 let img_id = next;
830 next += 1;
831 let smask = match &img.alpha {
832 Some(a) => {
833 let id = next;
834 next += 1;
835 let data = run_length(a);
836 let mut o = format!(
837 "<< /Type /XObject /Subtype /Image /Width {} /Height {} /ColorSpace /DeviceGray /BitsPerComponent 8 /Filter /RunLengthDecode /Length {} >>\nstream\n",
838 img.width,
839 img.height,
840 data.len()
841 )
842 .into_bytes();
843 o.extend_from_slice(&data);
844 o.extend_from_slice(b"\nendstream");
845 objects.push((id, o));
846 format!(" /SMask {id} 0 R")
847 }
848 None => String::new(),
849 };
850 let data = run_length(&img.rgb);
851 let mut o = format!(
852 "<< /Type /XObject /Subtype /Image /Width {} /Height {} /ColorSpace /DeviceRGB /BitsPerComponent 8{} /Filter /RunLengthDecode /Length {} >>\nstream\n",
853 img.width,
854 img.height,
855 smask,
856 data.len()
857 )
858 .into_bytes();
859 o.extend_from_slice(&data);
860 o.extend_from_slice(b"\nendstream");
861 objects.push((img_id, o));
862 let _ = write!(xobjects, "/Im{} {} 0 R ", k + 1, img_id);
863 }
864 let content_id = next;
865 let page_id = next + 1;
866 next += 2;
867 let body = std::mem::take(&mut page.content);
868 let mut o = format!("<< /Length {} >>\nstream\n", body.len()).into_bytes();
869 o.extend_from_slice(body.as_bytes());
870 o.extend_from_slice(b"endstream");
871 objects.push((content_id, o));
872 let xo = if xobjects.is_empty() { String::new() } else { format!(" /XObject << {xobjects}>>") };
873 let mut link_refs = String::new();
874 for link in std::mem::take(&mut page.links) {
875 if matches!(&link.target, LinkTarget::Page { index, .. } if *index >= page_count) {
876 continue;
877 }
878 let id = next;
879 next += 1;
880 let _ = write!(link_refs, "{id} 0 R ");
881 annotations.push((id, link));
882 }
883 let annots = if link_refs.is_empty() { String::new() } else { format!(" /Annots [{link_refs}]") };
884 objects.push((
885 page_id,
886 format!(
887 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 {} {}] /Resources << /ProcSet [/PDF /Text /ImageB /ImageC] /Font << {}>>{} >> /Contents {} 0 R{} >>",
888 n(page.width),
889 n(page.height),
890 font_dict,
891 xo,
892 content_id,
893 annots
894 )
895 .into_bytes(),
896 ));
897 kids.push(page_id);
898 }
899 for (id, link) in annotations {
900 let [x0, y0, x1, y1] = link.rect;
901 let action = match link.target {
902 LinkTarget::Uri(uri) => format!("/A << /S /URI /URI {} >>", pdf_uri_string(&uri)),
903 LinkTarget::Page { index, x, y } => {
904 let Some(&page_id) = kids.get(index) else { continue };
905 let height = self.pages.get(index).map_or(0.0, |p| p.height);
906 format!("/Dest [{page_id} 0 R /XYZ {} {} null]", n(x), n(height - y))
907 }
908 };
909 objects.push((
910 id,
911 format!("<< /Type /Annot /Subtype /Link /Rect [{} {} {} {}] /Border [0 0 0] {action} >>", n(x0), n(y0), n(x1), n(y1)).into_bytes(),
912 ));
913 }
914 objects.push((1, b"<< /Type /Catalog /Pages 2 0 R >>".to_vec()));
915 let kids_s: Vec<String> = kids.iter().map(|k| format!("{k} 0 R")).collect();
916 objects.push((2, format!("<< /Type /Pages /Kids [{}] /Count {} >>", kids_s.join(" "), kids.len()).into_bytes()));
917 let creator = if self.info.creator.is_empty() { "GridCraft" } else { &self.info.creator };
918 let mut info = format!("<< /Producer {} /Creator {}", pdf_text_string("GridCraft PDF"), pdf_text_string(creator));
919 if !self.info.title.is_empty() {
920 let _ = write!(info, " /Title {}", pdf_text_string(&self.info.title));
921 }
922 if !self.info.author.is_empty() {
923 let _ = write!(info, " /Author {}", pdf_text_string(&self.info.author));
924 }
925 info.push_str(" >>");
926 objects.push((3, info.into_bytes()));
927 objects.sort_by_key(|(id, _)| *id);
928
929 let mut out: Vec<u8> = Vec::new();
930 out.extend_from_slice(b"%PDF-1.7\n%\xE2\xE3\xCF\xD3\n");
931 let mut offsets = vec![0usize; next as usize];
932 for (id, body) in &objects {
933 if let Some(slot) = offsets.get_mut(*id as usize) {
934 *slot = out.len();
935 }
936 out.extend_from_slice(format!("{id} 0 obj\n").as_bytes());
937 out.extend_from_slice(body);
938 out.extend_from_slice(b"\nendobj\n");
939 }
940 let xref = out.len();
941 let mut x = format!("xref\n0 {}\n0000000000 65535 f \n", next);
942 for off in offsets.iter().skip(1) {
943 let _ = writeln!(x, "{off:010} 00000 n ");
944 }
945 let _ = write!(x, "trailer\n<< /Size {} /Root 1 0 R /Info 3 0 R >>\nstartxref\n{}\n%%EOF\n", next, xref);
946 out.extend_from_slice(x.as_bytes());
947 out
948 }
949}
950
951#[cfg(test)]
952mod tests;