use super::frame::FrameDrawing;
use super::project::{SheetDrawing, SheetText, ViewDrawing};
use super::svg::{ANNOTATION_WIDTH, CAP_PER_EM, EDGE_WIDTH, FRAME_WIDTH, HIGHLIGHT_RGB, HIGHLIGHT_WIDTH_FACTOR};
pub const PT_PER_MM: f64 = 72.0 / 25.4;
const BASE_FONT: &str = "Helvetica";
pub fn to_pdf(sheet: &SheetDrawing) -> Vec<u8> {
write_pdf(&sheet.name, &[sheet])
}
pub fn write_pdf(title: &str, pages: &[&SheetDrawing]) -> Vec<u8> {
write_pdf_3d(title, pages, None)
}
pub fn write_pdf_3d(title: &str, pages: &[&SheetDrawing], three_d: Option<&super::pdf3d::Pdf3d>) -> Vec<u8> {
let mut pdf = Writer::default();
pdf.header(if three_d.is_some() { "1.7" } else { "1.4" });
let first_page = 5;
let extra_page = three_d.and_then(|three_d| three_d.page.as_ref());
let page_count = pages.len() + usize::from(extra_page.is_some());
let plan = three_d.map(|three_d| super::pdf3d::Plan::new(three_d, first_page + page_count * 2, pages.len()));
let kids: Vec<String> =
(0..page_count).map(|index| format!("{} 0 R", first_page + index * 2)).collect();
pdf.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
pdf.object(
2,
&format!("<< /Type /Pages /Count {} /Kids [{}] >>", page_count, kids.join(" ")),
);
pdf.object(
3,
&format!(
"<< /Type /Font /Subtype /Type1 /BaseFont /{BASE_FONT} /Encoding /WinAnsiEncoding >>"
),
);
pdf.object(
4,
&format!(
"<< /Title ({}) /Producer (BREP) /HiddenLine ({}) >>",
string_literal(title),
string_literal(&hidden_line(pages))
),
);
for (index, sheet) in pages.iter().enumerate() {
let page = first_page + index * 2;
let contents = page + 1;
let (w, h) = (sheet.width_mm.max(1.0), sheet.height_mm.max(1.0));
let annots = plan.as_ref().map(|plan| plan.annots_on(index)).unwrap_or_default();
pdf.object(
page,
&format!(
"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 {:.4} {:.4}] \
/Resources << /Font << /F1 3 0 R >> >> /Contents {contents} 0 R{annots} >>",
w * PT_PER_MM,
h * PT_PER_MM
),
);
pdf.stream(contents, content_stream(sheet).as_bytes());
}
if let (Some(three_d), Some(plan)) = (three_d, plan.as_ref()) {
if let Some(extra) = extra_page {
let page = first_page + pages.len() * 2;
let (w, h) = (extra.width_mm.max(1.0), extra.height_mm.max(1.0));
pdf.object(
page,
&format!(
"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 {:.4} {:.4}] \
/Resources << /Font << /F1 3 0 R >> >> /Contents {} 0 R{} >>",
w * PT_PER_MM,
h * PT_PER_MM,
page + 1,
plan.annots_on(pages.len())
),
);
pdf.stream(page + 1, model_page_stream(extra).as_bytes());
}
for (number, body) in plan.objects(three_d, pages, first_page) {
pdf.object(number, &body);
}
}
pdf.xref_and_trailer(1);
pdf.out
}
fn hidden_line(pages: &[&SheetDrawing]) -> String {
let approximate: Vec<String> = pages
.iter()
.flat_map(|sheet| {
sheet
.views
.iter()
.filter(|view| view.hidden_line == super::project::HiddenLine::Mesh.label())
.map(move |view| format!("{} {}", sheet.name, view.id))
})
.collect();
if approximate.is_empty() {
super::project::HiddenLine::Analytic.label().to_string()
} else {
format!("{}: {}", super::project::HiddenLine::Mesh.label(), approximate.join(", "))
}
}
fn content_stream(sheet: &SheetDrawing) -> String {
let h = sheet.height_mm.max(1.0);
let mut out = String::new();
out.push_str("0 G\n0 g\n1 J\n1 j\n");
if let Some(frame) = &sheet.frame {
frame_marks(frame, h, &mut out);
}
for view in &sheet.views {
view_marks(view, h, &mut out);
}
for dimension in &sheet.dimensions {
if !dimension.lines.is_empty() {
out.push_str(&format!("{:.4} w\n", ANNOTATION_WIDTH * PT_PER_MM));
for line in &dimension.lines {
out.push_str(&path(line, h));
}
}
for text in &dimension.texts {
out.push_str(&text_marks(text, h));
}
}
for set in &sheet.ordinates {
if !set.lines.is_empty() {
out.push_str(&format!("{:.4} w\n", ANNOTATION_WIDTH * PT_PER_MM));
for line in &set.lines {
out.push_str(&path(line, h));
}
}
for text in &set.texts {
out.push_str(&text_marks(text, h));
}
}
out
}
fn model_page_stream(page: &super::pdf3d::Page3d) -> String {
let h = page.height_mm.max(1.0);
let mut out = String::from("0 G\n0 g\n1 J\n1 j\n");
out.push_str(&text_marks(&SheetText::flat(page.title.clone(), page.title_at, page.title_mm), h));
match &page.poster {
Some(poster) => view_marks(poster, h, &mut out),
None => {
let [x0, y0, x1, y1] = page.box_mm;
out.push_str(&format!("{:.4} w\n", FRAME_WIDTH * PT_PER_MM));
out.push_str(&path(&[[x0, y0], [x1, y0], [x1, y1], [x0, y1], [x0, y0]], h));
}
}
if !page.buttons.is_empty() {
out.push_str(&format!("{:.4} w\n", ANNOTATION_WIDTH * PT_PER_MM));
for button in &page.buttons {
let [x0, y0, x1, y1] = button.rect_mm;
out.push_str(&path(&[[x0, y0], [x1, y0], [x1, y1], [x0, y1], [x0, y0]], h));
let centre = [(x0 + x1) * 0.5, (y0 + y1) * 0.5];
out.push_str(&text_marks(&SheetText::flat(button.label.clone(), centre, page.button_text_mm), h));
}
}
for (line, at) in page.caption.iter().zip(&page.caption_at) {
out.push_str(&text_marks(&SheetText::flat(line.clone(), *at, page.caption_mm), h));
}
out
}
fn frame_marks(frame: &FrameDrawing, h: f64, out: &mut String) {
if !frame.lines.is_empty() {
out.push_str(&format!("{:.4} w\n", FRAME_WIDTH * PT_PER_MM));
for line in &frame.lines {
out.push_str(&path(line, h));
}
}
for text in &frame.texts {
out.push_str(&text_marks(text, h));
}
if let Some(table) = &frame.revision_table {
out.push_str(&format!("{:.4} w\n", FRAME_WIDTH * PT_PER_MM));
for line in &table.lines {
out.push_str(&path(line, h));
}
for text in &table.texts {
out.push_str(&text_marks(text, h));
}
}
}
fn line_art_marks(view: &ViewDrawing, layer: &super::project::LineArtLayer, h: f64, out: &mut String) {
use super::project::LineArtLayer as L;
let rgb = |c: [f64; 3]| format!("{:.3} {:.3} {:.3}", c[0], c[1], c[2]);
let [x0, y0, x1, y1] = layer.ground;
let (gx, gy) = point([x0, y1], h);
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));
out.push_str(&format!("{:.4} w 1 j\n", L::SEAM_MM * PT_PER_MM));
let mut tier = None;
for (triangle, adding) in &layer.fills {
if tier != Some(*adding) {
let c = rgb(if *adding { L::ADDING_FILL } else { L::PLACED_FILL });
out.push_str(&format!("{c} rg {c} RG\n"));
tier = Some(*adding);
}
for (index, p) in triangle.iter().enumerate() {
let (x, y) = point(*p, h);
out.push_str(&format!("{x:.3} {y:.3} {}\n", if index == 0 { "m" } else { "l" }));
}
out.push_str("h B\n");
}
let strokes = |runs: &mut dyn Iterator<Item = &Vec<[f64; 2]>>, color: [f64; 3], width: f64, out: &mut String| {
let mut runs = runs.peekable();
if runs.peek().is_none() {
return;
}
out.push_str(&format!("{} RG {:.4} w 1 J\n", rgb(color), width * PT_PER_MM));
for run in runs {
out.push_str(&path(run, h));
}
};
strokes(&mut view.edges.iter(), L::PLACED_EDGE, L::PLACED_EDGE_MM, out);
strokes(&mut layer.outlines.iter().filter(|(_, a)| !a).map(|(r, _)| r), L::PLACED_OUTLINE, L::PLACED_OUTLINE_MM, out);
strokes(&mut view.highlighted.iter(), L::INK, L::ADDING_EDGE_MM, out);
strokes(&mut layer.outlines.iter().filter(|(_, a)| *a).map(|(r, _)| r), L::INK, L::ADDING_OUTLINE_MM, out);
out.push_str(&format!("0 0 0 rg 0 0 0 RG {:.4} w 0 J\n", L::ARROW_SHAFT_MM * PT_PER_MM));
for (tail, tip) in &layer.arrows {
let Some((base, head)) = L::head(*tail, *tip) else { continue };
out.push_str(&path(&[*tail, base], h));
for (index, p) in head.iter().enumerate() {
let (x, y) = point(*p, h);
out.push_str(&format!("{x:.3} {y:.3} {}\n", if index == 0 { "m" } else { "l" }));
}
out.push_str("h f\n");
}
out.push_str("0 0 0 rg 0 0 0 RG 0 J 0 j\n");
}
fn view_marks(view: &ViewDrawing, h: f64, out: &mut String) {
if let Some(layer) = &view.line_art {
line_art_marks(view, layer, h, out);
} else if !view.edges.is_empty() {
out.push_str(&format!("{:.4} w\n", EDGE_WIDTH * PT_PER_MM));
for run in &view.edges {
out.push_str(&path(run, h));
}
}
if !view.highlighted.is_empty() && view.line_art.is_none() {
let [r, g, b] = HIGHLIGHT_RGB;
out.push_str(&format!("{r:.4} {g:.4} {b:.4} RG\n{:.4} w\n", EDGE_WIDTH * HIGHLIGHT_WIDTH_FACTOR * PT_PER_MM));
for run in &view.highlighted {
out.push_str(&path(run, h));
}
out.push_str("0 0 0 RG\n");
}
if let Some(section) = &view.section {
if !section.hatch.is_empty() {
out.push_str(&format!("{:.4} w\n", ANNOTATION_WIDTH * PT_PER_MM));
for line in §ion.hatch {
out.push_str(&path(line, h));
}
}
for text in §ion.texts {
out.push_str(&text_marks(text, h));
}
}
for mark in &view.section_marks {
if !mark.lines.is_empty() {
out.push_str(&format!("{:.4} w\n", FRAME_WIDTH * PT_PER_MM));
for line in &mark.lines {
out.push_str(&path(line, h));
}
}
for text in &mark.texts {
out.push_str(&text_marks(text, h));
}
}
if let Some(detail) = &view.detail {
out.push_str(&format!("{:.4} w\n", ANNOTATION_WIDTH * PT_PER_MM));
out.push_str(&path(&detail.circle, h));
for text in &detail.texts {
out.push_str(&text_marks(text, h));
}
}
for mark in &view.detail_marks {
if !mark.lines.is_empty() {
out.push_str(&format!("{:.4} w\n", FRAME_WIDTH * PT_PER_MM));
for line in &mark.lines {
out.push_str(&path(line, h));
}
}
for text in &mark.texts {
out.push_str(&text_marks(text, h));
}
}
for annotation in &view.annotations {
if !annotation.lines.is_empty() {
out.push_str(&format!("{:.4} w\n", ANNOTATION_WIDTH * PT_PER_MM));
for line in &annotation.lines {
out.push_str(&path(line, h));
}
}
for text in &annotation.texts {
out.push_str(&text_marks(text, h));
}
}
}
fn path(points: &[[f64; 2]], h: f64) -> String {
let mut out = String::new();
for (index, p) in points.iter().enumerate() {
let (x, y) = point(*p, h);
out.push_str(&format!("{x:.4} {y:.4} {}\n", if index == 0 { "m" } else { "l" }));
}
if !out.is_empty() {
out.push_str("S\n");
}
out
}
fn text_marks(text: &SheetText, h: f64) -> String {
let literal = string_literal(&text.text);
if literal.is_empty() || !(text.height > 1e-12) {
return String::new();
}
let size = (text.height / CAP_PER_EM) * PT_PER_MM;
let width = text_width(&text.text);
let (cx, cy) = point(text.anchor, h);
let unit = |v: [f64; 2]| [v[0] / text.height, -v[1] / text.height];
let (along, up) = (unit(text.right), unit(text.up));
let x = cx - (along[0] * width + up[0] * CAP_PER_EM) * size * 0.5;
let y = cy - (along[1] * width + up[1] * CAP_PER_EM) * size * 0.5;
format!(
"BT /F1 {size:.4} Tf {} ({literal}) Tj ET\n",
text_matrix(along, up, x, y)
)
}
fn text_matrix(along: [f64; 2], up: [f64; 2], x: f64, y: f64) -> String {
format!(
"{:.6} {:.6} {:.6} {:.6} {x:.4} {y:.4} Tm",
along[0], along[1], up[0], up[1]
)
}
fn point(p: [f64; 2], height_mm: f64) -> (f64, f64) {
(p[0] * PT_PER_MM, (height_mm - p[1]) * PT_PER_MM)
}
pub fn string_literal(text: &str) -> String {
let mut out = String::new();
for byte in encode_winansi(text) {
match byte {
b'(' | b')' | b'\\' => {
out.push('\\');
out.push(byte as char);
}
0x20..=0x7e => out.push(byte as char),
_ => out.push_str(&format!("\\{byte:03o}")),
}
}
out
}
pub fn encode_winansi(text: &str) -> Vec<u8> {
text.chars().map(|ch| winansi(ch).unwrap_or(b'?')).collect()
}
const CP1252_BAND: [(char, u8); 27] = [
('\u{20ac}', 0x80), ('\u{201a}', 0x82),
('\u{192}', 0x83),
('\u{201e}', 0x84),
('\u{2026}', 0x85), ('\u{2020}', 0x86),
('\u{2021}', 0x87),
('\u{2c6}', 0x88),
('\u{2030}', 0x89),
('\u{160}', 0x8a),
('\u{2039}', 0x8b),
('\u{152}', 0x8c),
('\u{17d}', 0x8e),
('\u{2018}', 0x91),
('\u{2019}', 0x92),
('\u{201c}', 0x93),
('\u{201d}', 0x94),
('\u{2022}', 0x95), ('\u{2013}', 0x96), ('\u{2014}', 0x97), ('\u{2dc}', 0x98),
('\u{2122}', 0x99), ('\u{161}', 0x9a),
('\u{203a}', 0x9b),
('\u{153}', 0x9c),
('\u{17e}', 0x9e),
('\u{178}', 0x9f),
];
fn winansi(ch: char) -> Option<u8> {
if let Some((_, byte)) = CP1252_BAND.iter().find(|(band, _)| *band == ch) {
return Some(*byte);
}
Some(match ch {
'\u{20}'..='\u{7e}' | '\u{a0}'..='\u{ff}' => ch as u8,
'\u{2300}' => 0xd8,
'\u{2212}' => b'-',
_ => return None,
})
}
pub fn hidden_line_of(pdf: &[u8]) -> Option<String> {
let key = b"/HiddenLine (";
let start = pdf.windows(key.len()).position(|w| w == key)? + key.len();
let mut out = Vec::new();
let mut i = start;
while i < pdf.len() && pdf[i] != b')' {
if pdf[i] == b'\\' && i + 1 < pdf.len() {
out.push(pdf[i + 1]);
i += 2;
} else {
out.push(pdf[i]);
i += 1;
}
}
Some(out.iter().map(|byte| from_winansi(*byte)).collect())
}
pub fn text_strings(pdf: &[u8]) -> Vec<String> {
let mut out = Vec::new();
let mut at = 0;
while let Some(open) = pdf[at..].iter().position(|b| *b == b'(').map(|i| at + i) {
let mut bytes = Vec::new();
let mut i = open + 1;
while i < pdf.len() && pdf[i] != b')' {
if pdf[i] == b'\\' && i + 1 < pdf.len() {
let digits: Vec<u8> = pdf[i + 1..].iter().take(3).take_while(|b| (b'0'..=b'7').contains(b)).copied().collect();
if digits.is_empty() {
bytes.push(pdf[i + 1]);
i += 2;
} else {
bytes.push(digits.iter().fold(0u32, |n, d| n * 8 + (d - b'0') as u32) as u8);
i += 1 + digits.len();
}
} else {
bytes.push(pdf[i]);
i += 1;
}
}
at = i + 1;
if pdf[at.min(pdf.len())..].starts_with(b" Tj") {
out.push(bytes.iter().map(|byte| from_winansi(*byte)).collect());
}
}
out
}
fn from_winansi(byte: u8) -> char {
CP1252_BAND.iter().find(|(_, band)| *band == byte).map(|(ch, _)| *ch).unwrap_or(byte as char)
}
pub fn text_width(text: &str) -> f64 {
encode_winansi(text).iter().map(|byte| advance(*byte)).sum::<f64>() / 1000.0
}
fn advance(byte: u8) -> f64 {
const ASCII: [u16; 95] = [
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, ];
match byte {
0x20..=0x7e => f64::from(ASCII[(byte - 0x20) as usize]),
0xb0 => 400.0, 0xb1 => 584.0, 0xd7 => 584.0, 0xd8 => 778.0, 0xa0 => 278.0, 0xc0..=0xdd => 722.0,
_ => 556.0,
}
}
#[derive(Default)]
struct Writer {
out: Vec<u8>,
offsets: Vec<usize>,
}
impl Writer {
fn header(&mut self, version: &str) {
self.out.extend_from_slice(format!("%PDF-{version}\n").as_bytes());
}
fn begin(&mut self, number: usize) {
assert_eq!(number, self.offsets.len() + 1, "objects are written in order");
self.offsets.push(self.out.len());
self.out.extend_from_slice(format!("{number} 0 obj\n").as_bytes());
}
fn object(&mut self, number: usize, body: &str) {
self.begin(number);
self.out.extend_from_slice(body.as_bytes());
self.out.extend_from_slice(b"\nendobj\n");
}
fn stream(&mut self, number: usize, content: &[u8]) {
self.begin(number);
self.out
.extend_from_slice(format!("<< /Length {} >>\nstream\n", content.len()).as_bytes());
self.out.extend_from_slice(content);
self.out.extend_from_slice(b"endstream\nendobj\n");
}
fn xref_and_trailer(&mut self, root: usize) {
let start = self.out.len();
let count = self.offsets.len() + 1;
self.out.extend_from_slice(format!("xref\n0 {count}\n").as_bytes());
self.out.extend_from_slice(b"0000000000 65535 f \n");
for offset in &self.offsets {
self.out.extend_from_slice(format!("{offset:010} 00000 n \n").as_bytes());
}
self.out.extend_from_slice(
format!("trailer\n<< /Size {count} /Root {root} 0 R /Info 4 0 R >>\nstartxref\n{start}\n%%EOF\n")
.as_bytes(),
);
}
}