1use super::frame::FrameDrawing;
89use super::project::{SheetDrawing, SheetText, ViewDrawing};
90use super::svg::{ANNOTATION_WIDTH, CAP_PER_EM, EDGE_WIDTH, FRAME_WIDTH, HIGHLIGHT_RGB, HIGHLIGHT_WIDTH_FACTOR};
91
92pub const PT_PER_MM: f64 = 72.0 / 25.4;
94
95const BASE_FONT: &str = "Helvetica";
97
98pub fn to_pdf(sheet: &SheetDrawing) -> Vec<u8> {
101 write_pdf(&sheet.name, &[sheet])
102}
103
104pub fn write_pdf(title: &str, pages: &[&SheetDrawing]) -> Vec<u8> {
107 write_pdf_3d(title, pages, None)
108}
109
110pub fn write_pdf_3d(title: &str, pages: &[&SheetDrawing], three_d: Option<&super::pdf3d::Pdf3d>) -> Vec<u8> {
128 let mut pdf = Writer::default();
129 pdf.header(if three_d.is_some() { "1.7" } else { "1.4" });
130 let first_page = 5;
134 let extra_page = three_d.and_then(|three_d| three_d.page.as_ref());
135 let page_count = pages.len() + usize::from(extra_page.is_some());
136 let plan = three_d.map(|three_d| super::pdf3d::Plan::new(three_d, first_page + page_count * 2, pages.len()));
137 let kids: Vec<String> =
138 (0..page_count).map(|index| format!("{} 0 R", first_page + index * 2)).collect();
139 pdf.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
140 pdf.object(
141 2,
142 &format!("<< /Type /Pages /Count {} /Kids [{}] >>", page_count, kids.join(" ")),
143 );
144 pdf.object(
145 3,
146 &format!(
147 "<< /Type /Font /Subtype /Type1 /BaseFont /{BASE_FONT} /Encoding /WinAnsiEncoding >>"
148 ),
149 );
150 pdf.object(
151 4,
152 &format!(
153 "<< /Title ({}) /Producer (BREP) /HiddenLine ({}) >>",
154 string_literal(title),
155 string_literal(&hidden_line(pages))
156 ),
157 );
158 for (index, sheet) in pages.iter().enumerate() {
159 let page = first_page + index * 2;
160 let contents = page + 1;
161 let (w, h) = (sheet.width_mm.max(1.0), sheet.height_mm.max(1.0));
162 let annots = plan.as_ref().map(|plan| plan.annots_on(index)).unwrap_or_default();
163 pdf.object(
164 page,
165 &format!(
166 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 {:.4} {:.4}] \
167 /Resources << /Font << /F1 3 0 R >> >> /Contents {contents} 0 R{annots} >>",
168 w * PT_PER_MM,
169 h * PT_PER_MM
170 ),
171 );
172 pdf.stream(contents, content_stream(sheet).as_bytes());
173 }
174 if let (Some(three_d), Some(plan)) = (three_d, plan.as_ref()) {
175 if let Some(extra) = extra_page {
176 let page = first_page + pages.len() * 2;
177 let (w, h) = (extra.width_mm.max(1.0), extra.height_mm.max(1.0));
178 pdf.object(
179 page,
180 &format!(
181 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 {:.4} {:.4}] \
182 /Resources << /Font << /F1 3 0 R >> >> /Contents {} 0 R{} >>",
183 w * PT_PER_MM,
184 h * PT_PER_MM,
185 page + 1,
186 plan.annots_on(pages.len())
187 ),
188 );
189 pdf.stream(page + 1, model_page_stream(extra).as_bytes());
190 }
191 for (number, body) in plan.objects(three_d, pages, first_page) {
192 pdf.object(number, &body);
193 }
194 }
195 pdf.xref_and_trailer(1);
196 pdf.out
197}
198
199fn hidden_line(pages: &[&SheetDrawing]) -> String {
202 let approximate: Vec<String> = pages
203 .iter()
204 .flat_map(|sheet| {
205 sheet
206 .views
207 .iter()
208 .filter(|view| view.hidden_line == super::project::HiddenLine::Mesh.label())
209 .map(move |view| format!("{} {}", sheet.name, view.id))
210 })
211 .collect();
212 if approximate.is_empty() {
213 super::project::HiddenLine::Analytic.label().to_string()
214 } else {
215 format!("{}: {}", super::project::HiddenLine::Mesh.label(), approximate.join(", "))
216 }
217}
218
219fn content_stream(sheet: &SheetDrawing) -> String {
226 let h = sheet.height_mm.max(1.0);
227 let mut out = String::new();
228 out.push_str("0 G\n0 g\n1 J\n1 j\n");
231 if let Some(frame) = &sheet.frame {
232 frame_marks(frame, h, &mut out);
233 }
234 for view in &sheet.views {
235 view_marks(view, h, &mut out);
236 }
237 for dimension in &sheet.dimensions {
240 if !dimension.lines.is_empty() {
241 out.push_str(&format!("{:.4} w\n", ANNOTATION_WIDTH * PT_PER_MM));
242 for line in &dimension.lines {
243 out.push_str(&path(line, h));
244 }
245 }
246 for text in &dimension.texts {
247 out.push_str(&text_marks(text, h));
248 }
249 }
250 for set in &sheet.ordinates {
252 if !set.lines.is_empty() {
253 out.push_str(&format!("{:.4} w\n", ANNOTATION_WIDTH * PT_PER_MM));
254 for line in &set.lines {
255 out.push_str(&path(line, h));
256 }
257 }
258 for text in &set.texts {
259 out.push_str(&text_marks(text, h));
260 }
261 }
262 out
263}
264
265fn model_page_stream(page: &super::pdf3d::Page3d) -> String {
269 let h = page.height_mm.max(1.0);
270 let mut out = String::from("0 G\n0 g\n1 J\n1 j\n");
271 out.push_str(&text_marks(&SheetText::flat(page.title.clone(), page.title_at, page.title_mm), h));
272 match &page.poster {
273 Some(poster) => view_marks(poster, h, &mut out),
274 None => {
275 let [x0, y0, x1, y1] = page.box_mm;
278 out.push_str(&format!("{:.4} w\n", FRAME_WIDTH * PT_PER_MM));
279 out.push_str(&path(&[[x0, y0], [x1, y0], [x1, y1], [x0, y1], [x0, y0]], h));
280 }
281 }
282 if !page.buttons.is_empty() {
283 out.push_str(&format!("{:.4} w\n", ANNOTATION_WIDTH * PT_PER_MM));
284 for button in &page.buttons {
285 let [x0, y0, x1, y1] = button.rect_mm;
286 out.push_str(&path(&[[x0, y0], [x1, y0], [x1, y1], [x0, y1], [x0, y0]], h));
287 let centre = [(x0 + x1) * 0.5, (y0 + y1) * 0.5];
288 out.push_str(&text_marks(&SheetText::flat(button.label.clone(), centre, page.button_text_mm), h));
289 }
290 }
291 for (line, at) in page.caption.iter().zip(&page.caption_at) {
292 out.push_str(&text_marks(&SheetText::flat(line.clone(), *at, page.caption_mm), h));
293 }
294 out
295}
296
297fn frame_marks(frame: &FrameDrawing, h: f64, out: &mut String) {
298 if !frame.lines.is_empty() {
299 out.push_str(&format!("{:.4} w\n", FRAME_WIDTH * PT_PER_MM));
300 for line in &frame.lines {
301 out.push_str(&path(line, h));
302 }
303 }
304 for text in &frame.texts {
305 out.push_str(&text_marks(text, h));
306 }
307 if let Some(table) = &frame.revision_table {
310 out.push_str(&format!("{:.4} w\n", FRAME_WIDTH * PT_PER_MM));
311 for line in &table.lines {
312 out.push_str(&path(line, h));
313 }
314 for text in &table.texts {
315 out.push_str(&text_marks(text, h));
316 }
317 }
318}
319
320fn line_art_marks(view: &ViewDrawing, layer: &super::project::LineArtLayer, h: f64, out: &mut String) {
324 use super::project::LineArtLayer as L;
325 let rgb = |c: [f64; 3]| format!("{:.3} {:.3} {:.3}", c[0], c[1], c[2]);
326 let [x0, y0, x1, y1] = layer.ground;
327 let (gx, gy) = point([x0, y1], h);
328 out.push_str(&format!("{} rg\n{gx:.3} {gy:.3} {:.3} {:.3} re f\n", rgb(L::GROUND), (x1 - x0) * PT_PER_MM, (y1 - y0) * PT_PER_MM));
329 out.push_str(&format!("{:.4} w 1 j\n", L::SEAM_MM * PT_PER_MM));
330 let mut tier = None;
331 for (triangle, adding) in &layer.fills {
332 if tier != Some(*adding) {
333 let c = rgb(if *adding { L::ADDING_FILL } else { L::PLACED_FILL });
334 out.push_str(&format!("{c} rg {c} RG\n"));
335 tier = Some(*adding);
336 }
337 for (index, p) in triangle.iter().enumerate() {
338 let (x, y) = point(*p, h);
339 out.push_str(&format!("{x:.3} {y:.3} {}\n", if index == 0 { "m" } else { "l" }));
340 }
341 out.push_str("h B\n");
342 }
343 let strokes = |runs: &mut dyn Iterator<Item = &Vec<[f64; 2]>>, color: [f64; 3], width: f64, out: &mut String| {
344 let mut runs = runs.peekable();
345 if runs.peek().is_none() {
346 return;
347 }
348 out.push_str(&format!("{} RG {:.4} w 1 J\n", rgb(color), width * PT_PER_MM));
349 for run in runs {
350 out.push_str(&path(run, h));
351 }
352 };
353 strokes(&mut view.edges.iter(), L::PLACED_EDGE, L::PLACED_EDGE_MM, out);
354 strokes(&mut layer.outlines.iter().filter(|(_, a)| !a).map(|(r, _)| r), L::PLACED_OUTLINE, L::PLACED_OUTLINE_MM, out);
355 strokes(&mut view.highlighted.iter(), L::INK, L::ADDING_EDGE_MM, out);
356 strokes(&mut layer.outlines.iter().filter(|(_, a)| *a).map(|(r, _)| r), L::INK, L::ADDING_OUTLINE_MM, out);
357 out.push_str(&format!("0 0 0 rg 0 0 0 RG {:.4} w 0 J\n", L::ARROW_SHAFT_MM * PT_PER_MM));
358 for (tail, tip) in &layer.arrows {
359 let Some((base, head)) = L::head(*tail, *tip) else { continue };
360 out.push_str(&path(&[*tail, base], h));
361 for (index, p) in head.iter().enumerate() {
362 let (x, y) = point(*p, h);
363 out.push_str(&format!("{x:.3} {y:.3} {}\n", if index == 0 { "m" } else { "l" }));
364 }
365 out.push_str("h f\n");
366 }
367 out.push_str("0 0 0 rg 0 0 0 RG 0 J 0 j\n");
368}
369
370fn view_marks(view: &ViewDrawing, h: f64, out: &mut String) {
371 if let Some(layer) = &view.line_art {
372 line_art_marks(view, layer, h, out);
373 } else if !view.edges.is_empty() {
374 out.push_str(&format!("{:.4} w\n", EDGE_WIDTH * PT_PER_MM));
375 for run in &view.edges {
376 out.push_str(&path(run, h));
377 }
378 }
379 if !view.highlighted.is_empty() && view.line_art.is_none() {
380 let [r, g, b] = HIGHLIGHT_RGB;
382 out.push_str(&format!("{r:.4} {g:.4} {b:.4} RG\n{:.4} w\n", EDGE_WIDTH * HIGHLIGHT_WIDTH_FACTOR * PT_PER_MM));
383 for run in &view.highlighted {
384 out.push_str(&path(run, h));
385 }
386 out.push_str("0 0 0 RG\n");
387 }
388 if let Some(section) = &view.section {
391 if !section.hatch.is_empty() {
392 out.push_str(&format!("{:.4} w\n", ANNOTATION_WIDTH * PT_PER_MM));
393 for line in §ion.hatch {
394 out.push_str(&path(line, h));
395 }
396 }
397 for text in §ion.texts {
398 out.push_str(&text_marks(text, h));
399 }
400 }
401 for mark in &view.section_marks {
402 if !mark.lines.is_empty() {
403 out.push_str(&format!("{:.4} w\n", FRAME_WIDTH * PT_PER_MM));
404 for line in &mark.lines {
405 out.push_str(&path(line, h));
406 }
407 }
408 for text in &mark.texts {
409 out.push_str(&text_marks(text, h));
410 }
411 }
412 if let Some(detail) = &view.detail {
415 out.push_str(&format!("{:.4} w\n", ANNOTATION_WIDTH * PT_PER_MM));
416 out.push_str(&path(&detail.circle, h));
417 for text in &detail.texts {
418 out.push_str(&text_marks(text, h));
419 }
420 }
421 for mark in &view.detail_marks {
422 if !mark.lines.is_empty() {
423 out.push_str(&format!("{:.4} w\n", FRAME_WIDTH * PT_PER_MM));
424 for line in &mark.lines {
425 out.push_str(&path(line, h));
426 }
427 }
428 for text in &mark.texts {
429 out.push_str(&text_marks(text, h));
430 }
431 }
432 for annotation in &view.annotations {
433 if !annotation.lines.is_empty() {
434 out.push_str(&format!("{:.4} w\n", ANNOTATION_WIDTH * PT_PER_MM));
435 for line in &annotation.lines {
436 out.push_str(&path(line, h));
437 }
438 }
439 for text in &annotation.texts {
440 out.push_str(&text_marks(text, h));
441 }
442 }
443}
444
445fn path(points: &[[f64; 2]], h: f64) -> String {
447 let mut out = String::new();
448 for (index, p) in points.iter().enumerate() {
449 let (x, y) = point(*p, h);
450 out.push_str(&format!("{x:.4} {y:.4} {}\n", if index == 0 { "m" } else { "l" }));
451 }
452 if !out.is_empty() {
453 out.push_str("S\n");
454 }
455 out
456}
457
458fn text_marks(text: &SheetText, h: f64) -> String {
465 let literal = string_literal(&text.text);
466 if literal.is_empty() || !(text.height > 1e-12) {
467 return String::new();
468 }
469 let size = (text.height / CAP_PER_EM) * PT_PER_MM;
470 let width = text_width(&text.text);
471 let (cx, cy) = point(text.anchor, h);
472 let unit = |v: [f64; 2]| [v[0] / text.height, -v[1] / text.height];
478 let (along, up) = (unit(text.right), unit(text.up));
479 let x = cx - (along[0] * width + up[0] * CAP_PER_EM) * size * 0.5;
482 let y = cy - (along[1] * width + up[1] * CAP_PER_EM) * size * 0.5;
483 format!(
484 "BT /F1 {size:.4} Tf {} ({literal}) Tj ET\n",
485 text_matrix(along, up, x, y)
486 )
487}
488
489fn text_matrix(along: [f64; 2], up: [f64; 2], x: f64, y: f64) -> String {
492 format!(
493 "{:.6} {:.6} {:.6} {:.6} {x:.4} {y:.4} Tm",
494 along[0], along[1], up[0], up[1]
495 )
496}
497
498fn point(p: [f64; 2], height_mm: f64) -> (f64, f64) {
500 (p[0] * PT_PER_MM, (height_mm - p[1]) * PT_PER_MM)
501}
502
503pub fn string_literal(text: &str) -> String {
511 let mut out = String::new();
512 for byte in encode_winansi(text) {
513 match byte {
514 b'(' | b')' | b'\\' => {
515 out.push('\\');
516 out.push(byte as char);
517 }
518 0x20..=0x7e => out.push(byte as char),
519 _ => out.push_str(&format!("\\{byte:03o}")),
520 }
521 }
522 out
523}
524
525pub fn encode_winansi(text: &str) -> Vec<u8> {
528 text.chars().map(|ch| winansi(ch).unwrap_or(b'?')).collect()
529}
530
531const CP1252_BAND: [(char, u8); 27] = [
534 ('\u{20ac}', 0x80), ('\u{201a}', 0x82),
536 ('\u{192}', 0x83),
537 ('\u{201e}', 0x84),
538 ('\u{2026}', 0x85), ('\u{2020}', 0x86),
540 ('\u{2021}', 0x87),
541 ('\u{2c6}', 0x88),
542 ('\u{2030}', 0x89),
543 ('\u{160}', 0x8a),
544 ('\u{2039}', 0x8b),
545 ('\u{152}', 0x8c),
546 ('\u{17d}', 0x8e),
547 ('\u{2018}', 0x91),
548 ('\u{2019}', 0x92),
549 ('\u{201c}', 0x93),
550 ('\u{201d}', 0x94),
551 ('\u{2022}', 0x95), ('\u{2013}', 0x96), ('\u{2014}', 0x97), ('\u{2dc}', 0x98),
555 ('\u{2122}', 0x99), ('\u{161}', 0x9a),
557 ('\u{203a}', 0x9b),
558 ('\u{153}', 0x9c),
559 ('\u{17e}', 0x9e),
560 ('\u{178}', 0x9f),
561];
562
563fn winansi(ch: char) -> Option<u8> {
565 if let Some((_, byte)) = CP1252_BAND.iter().find(|(band, _)| *band == ch) {
566 return Some(*byte);
567 }
568 Some(match ch {
569 '\u{20}'..='\u{7e}' | '\u{a0}'..='\u{ff}' => ch as u8,
572 '\u{2300}' => 0xd8,
577 '\u{2212}' => b'-',
578 _ => return None,
579 })
580}
581
582pub fn hidden_line_of(pdf: &[u8]) -> Option<String> {
591 let key = b"/HiddenLine (";
592 let start = pdf.windows(key.len()).position(|w| w == key)? + key.len();
593 let mut out = Vec::new();
594 let mut i = start;
595 while i < pdf.len() && pdf[i] != b')' {
596 if pdf[i] == b'\\' && i + 1 < pdf.len() {
597 out.push(pdf[i + 1]);
598 i += 2;
599 } else {
600 out.push(pdf[i]);
601 i += 1;
602 }
603 }
604 Some(out.iter().map(|byte| from_winansi(*byte)).collect())
605}
606
607pub fn text_strings(pdf: &[u8]) -> Vec<String> {
608 let mut out = Vec::new();
609 let mut at = 0;
610 while let Some(open) = pdf[at..].iter().position(|b| *b == b'(').map(|i| at + i) {
611 let mut bytes = Vec::new();
612 let mut i = open + 1;
613 while i < pdf.len() && pdf[i] != b')' {
614 if pdf[i] == b'\\' && i + 1 < pdf.len() {
615 let digits: Vec<u8> = pdf[i + 1..].iter().take(3).take_while(|b| (b'0'..=b'7').contains(b)).copied().collect();
616 if digits.is_empty() {
617 bytes.push(pdf[i + 1]);
618 i += 2;
619 } else {
620 bytes.push(digits.iter().fold(0u32, |n, d| n * 8 + (d - b'0') as u32) as u8);
621 i += 1 + digits.len();
622 }
623 } else {
624 bytes.push(pdf[i]);
625 i += 1;
626 }
627 }
628 at = i + 1;
629 if pdf[at.min(pdf.len())..].starts_with(b" Tj") {
630 out.push(bytes.iter().map(|byte| from_winansi(*byte)).collect());
631 }
632 }
633 out
634}
635
636fn from_winansi(byte: u8) -> char {
638 CP1252_BAND.iter().find(|(_, band)| *band == byte).map(|(ch, _)| *ch).unwrap_or(byte as char)
639}
640
641pub fn text_width(text: &str) -> f64 {
643 encode_winansi(text).iter().map(|byte| advance(*byte)).sum::<f64>() / 1000.0
644}
645
646fn advance(byte: u8) -> f64 {
652 const ASCII: [u16; 95] = [
653 278, 278, 355, 556, 556, 889, 667, 191, 333, 333, 389, 584, 278, 333, 278, 278, 556, 556, 556, 556, 556, 556, 556, 556, 556, 556, 278, 278, 584, 584, 584, 556, 1015, 667, 667, 722, 722, 667, 611, 778, 722, 278, 500, 667, 556, 833, 722, 778, 667, 778, 722, 667, 611, 722, 667, 944, 667, 667, 611, 278, 278, 278, 469, 556, 333, 556, 556, 500, 556, 556, 278, 556, 556, 222, 222, 500, 222, 833, 556, 556, 556, 556, 333, 500, 278, 556, 500, 722, 500, 500, 500, 334, 260, 334, 584, ];
660 match byte {
661 0x20..=0x7e => f64::from(ASCII[(byte - 0x20) as usize]),
662 0xb0 => 400.0, 0xb1 => 584.0, 0xd7 => 584.0, 0xd8 => 778.0, 0xa0 => 278.0, 0xc0..=0xdd => 722.0,
671 _ => 556.0,
672 }
673}
674
675#[derive(Default)]
682struct Writer {
683 out: Vec<u8>,
684 offsets: Vec<usize>,
686}
687
688impl Writer {
689 fn header(&mut self, version: &str) {
690 self.out.extend_from_slice(format!("%PDF-{version}\n").as_bytes());
691 }
696
697 fn begin(&mut self, number: usize) {
698 assert_eq!(number, self.offsets.len() + 1, "objects are written in order");
699 self.offsets.push(self.out.len());
700 self.out.extend_from_slice(format!("{number} 0 obj\n").as_bytes());
701 }
702
703 fn object(&mut self, number: usize, body: &str) {
704 self.begin(number);
705 self.out.extend_from_slice(body.as_bytes());
706 self.out.extend_from_slice(b"\nendobj\n");
707 }
708
709 fn stream(&mut self, number: usize, content: &[u8]) {
710 self.begin(number);
711 self.out
712 .extend_from_slice(format!("<< /Length {} >>\nstream\n", content.len()).as_bytes());
713 self.out.extend_from_slice(content);
714 self.out.extend_from_slice(b"endstream\nendobj\n");
715 }
716
717 fn xref_and_trailer(&mut self, root: usize) {
721 let start = self.out.len();
722 let count = self.offsets.len() + 1;
723 self.out.extend_from_slice(format!("xref\n0 {count}\n").as_bytes());
724 self.out.extend_from_slice(b"0000000000 65535 f \n");
725 for offset in &self.offsets {
726 self.out.extend_from_slice(format!("{offset:010} 00000 n \n").as_bytes());
727 }
728 self.out.extend_from_slice(
729 format!("trailer\n<< /Size {count} /Root {root} 0 R /Info 4 0 R >>\nstartxref\n{start}\n%%EOF\n")
730 .as_bytes(),
731 );
732 }
733}
734
735
736
737