1use crate::config::project::PdfConfig;
2use crate::parser::{Program, Declaration, Statement, UIElement, ComponentRef, Expr, StringPart};
3
4const HELVETICA_WIDTHS: [u16; 95] = [
6 278, 278, 355, 556, 556, 889, 667, 191, 333, 333, 389, 584, 278, 333, 278, 278,
7 556, 556, 556, 556, 556, 556, 556, 556, 556, 556, 278, 278, 584, 584, 584, 556,
8 1015, 667, 667, 722, 722, 667, 611, 778, 722, 278, 500, 667, 556, 833, 722, 778,
9 667, 778, 722, 667, 611, 722, 667, 944, 667, 667, 611, 278, 278, 278, 469, 556,
10 333, 556, 556, 500, 556, 556, 278, 556, 556, 222, 222, 500, 222, 833, 556, 556,
11 556, 556, 333, 500, 278, 556, 500, 722, 500, 500, 500, 334, 260, 334, 584,
12];
13
14const HELVETICA_BOLD_WIDTHS: [u16; 95] = [
15 278, 333, 474, 556, 556, 889, 722, 238, 333, 333, 389, 584, 278, 333, 278, 278,
16 556, 556, 556, 556, 556, 556, 556, 556, 556, 556, 333, 333, 584, 584, 584, 611,
17 975, 722, 722, 722, 722, 667, 611, 778, 722, 278, 556, 722, 611, 833, 722, 778,
18 667, 778, 722, 667, 611, 722, 667, 944, 667, 667, 611, 333, 278, 333, 584, 556,
19 333, 556, 611, 556, 611, 556, 333, 611, 611, 278, 278, 556, 278, 889, 611, 611,
20 611, 611, 389, 556, 333, 611, 556, 778, 556, 556, 500, 389, 280, 389, 584,
21];
22
23const TIMES_WIDTHS: [u16; 95] = [
24 250, 333, 408, 500, 500, 833, 778, 180, 333, 333, 500, 564, 250, 333, 250, 278,
25 500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 278, 278, 564, 564, 564, 444,
26 921, 722, 667, 667, 722, 611, 556, 722, 722, 333, 389, 722, 611, 889, 722, 722,
27 556, 722, 667, 556, 611, 722, 722, 944, 722, 722, 611, 333, 278, 333, 469, 500,
28 333, 444, 500, 444, 500, 444, 333, 500, 500, 278, 278, 500, 278, 778, 500, 500,
29 500, 500, 333, 389, 278, 500, 500, 722, 500, 500, 444, 480, 200, 480, 541,
30];
31
32const COURIER_WIDTH: u16 = 600;
33
34fn char_width(ch: char, font: &str) -> u16 {
35 let code = ch as u32;
36 if code < 32 || code > 126 {
37 return 500;
38 }
39 let idx = (code - 32) as usize;
40 if font.contains("Courier") {
41 COURIER_WIDTH
42 } else if font.contains("Times") {
43 TIMES_WIDTHS[idx]
44 } else if font.contains("Bold") {
45 HELVETICA_BOLD_WIDTHS[idx]
46 } else {
47 HELVETICA_WIDTHS[idx]
48 }
49}
50
51fn text_width(text: &str, font: &str, font_size: f64) -> f64 {
52 let units: u32 = text.chars().map(|c| char_width(c, font) as u32).sum();
53 units as f64 * font_size / 1000.0
54}
55
56fn truncate_text(text: &str, font: &str, font_size: f64, max_width: f64) -> String {
57 let full_w = text_width(text, font, font_size);
58 if full_w <= max_width {
59 return text.to_string();
60 }
61 let ellipsis_w = text_width("...", font, font_size);
62 let target = max_width - ellipsis_w;
63 if target <= 0.0 {
64 return "...".to_string();
65 }
66 let mut w = 0.0;
67 let mut end = 0;
68 for (i, ch) in text.char_indices() {
69 let cw = char_width(ch, font) as f64 * font_size / 1000.0;
70 if w + cw > target { break; }
71 w += cw;
72 end = i + ch.len_utf8();
73 }
74 format!("{}...", &text[..end])
75}
76
77fn page_dimensions(size: &str) -> (f64, f64) {
78 match size.to_uppercase().as_str() {
79 "A4" => (595.28, 841.89),
80 "A3" => (841.89, 1190.55),
81 "A5" => (419.53, 595.28),
82 "LETTER" => (612.0, 792.0),
83 "LEGAL" => (612.0, 1008.0),
84 _ => (595.28, 841.89),
85 }
86}
87
88struct PdfObj {
91 id: usize,
92 data: Vec<u8>,
93}
94
95struct ContentStream {
98 ops: Vec<u8>,
99}
100
101impl ContentStream {
102 fn new() -> Self { Self { ops: Vec::new() } }
103
104 fn op(&mut self, s: &str) {
105 self.ops.extend_from_slice(s.as_bytes());
106 self.ops.push(b'\n');
107 }
108
109 fn set_font(&mut self, tag: &str, size: f64) {
110 self.op(&format!("/{} {} Tf", tag, fmt_f64(size)));
111 }
112 fn set_color(&mut self, r: f64, g: f64, b: f64) {
113 self.op(&format!("{} {} {} rg", fmt_f64(r), fmt_f64(g), fmt_f64(b)));
114 }
115 fn set_stroke_color(&mut self, r: f64, g: f64, b: f64) {
116 self.op(&format!("{} {} {} RG", fmt_f64(r), fmt_f64(g), fmt_f64(b)));
117 }
118 fn begin_text(&mut self) { self.op("BT"); }
119 fn end_text(&mut self) { self.op("ET"); }
120 fn text_position(&mut self, x: f64, y: f64) {
121 self.op(&format!("{} {} Td", fmt_f64(x), fmt_f64(y)));
122 }
123 fn show_text(&mut self, text: &str) {
124 self.op(&format!("<{}> Tj", text_to_pdf_hex(text)));
125 }
126 fn text_at(&mut self, x: f64, y: f64, font_tag: &str, size: f64, text: &str) {
127 self.begin_text();
128 self.set_font(font_tag, size);
129 self.text_position(x, y);
130 self.show_text(text);
131 self.end_text();
132 }
133 fn rect(&mut self, x: f64, y: f64, w: f64, h: f64) {
134 self.op(&format!("{} {} {} {} re", fmt_f64(x), fmt_f64(y), fmt_f64(w), fmt_f64(h)));
135 }
136 fn fill(&mut self) { self.op("f"); }
137 fn stroke(&mut self) { self.op("S"); }
138 fn line(&mut self, x1: f64, y1: f64, x2: f64, y2: f64) {
139 self.op(&format!("{} {} m {} {} l S", fmt_f64(x1), fmt_f64(y1), fmt_f64(x2), fmt_f64(y2)));
140 }
141 fn set_line_width(&mut self, w: f64) {
142 self.op(&format!("{} w", fmt_f64(w)));
143 }
144 fn rounded_rect(&mut self, x: f64, y: f64, w: f64, h: f64, r: f64) {
145 let r = r.min(w / 2.0).min(h / 2.0);
146 if r < 0.5 { self.rect(x, y, w, h); return; }
147 let k = 0.5523;
148 let kr = k * r;
149 self.op(&format!("{} {} m", fmt_f64(x + r), fmt_f64(y)));
150 self.op(&format!("{} {} l", fmt_f64(x + w - r), fmt_f64(y)));
151 self.op(&format!("{} {} {} {} {} {} c", fmt_f64(x+w-r+kr), fmt_f64(y), fmt_f64(x+w), fmt_f64(y+r-kr), fmt_f64(x+w), fmt_f64(y+r)));
152 self.op(&format!("{} {} l", fmt_f64(x + w), fmt_f64(y + h - r)));
153 self.op(&format!("{} {} {} {} {} {} c", fmt_f64(x+w), fmt_f64(y+h-r+kr), fmt_f64(x+w-r+kr), fmt_f64(y+h), fmt_f64(x+w-r), fmt_f64(y+h)));
154 self.op(&format!("{} {} l", fmt_f64(x + r), fmt_f64(y + h)));
155 self.op(&format!("{} {} {} {} {} {} c", fmt_f64(x+r-kr), fmt_f64(y+h), fmt_f64(x), fmt_f64(y+h-r+kr), fmt_f64(x), fmt_f64(y+h-r)));
156 self.op(&format!("{} {} l", fmt_f64(x), fmt_f64(y + r)));
157 self.op(&format!("{} {} {} {} {} {} c", fmt_f64(x), fmt_f64(y+r-kr), fmt_f64(x+r-kr), fmt_f64(y), fmt_f64(x+r), fmt_f64(y)));
158 self.op("h");
159 }
160 fn bytes(&self) -> &[u8] { &self.ops }
161}
162
163pub struct PdfCodegen {
176 objects: Vec<PdfObj>,
177 pages: Vec<usize>,
178 page_width: f64,
179 page_height: f64,
180 margin_top: f64,
181 margin_bottom: f64,
182 margin_left: f64,
183 margin_right: f64,
184 cursor_y: f64,
185 current_stream: ContentStream,
186 fonts: Vec<(String, String)>,
187 current_font: String,
188 current_font_name: String,
189 current_font_size: f64,
190 current_color: (f64, f64, f64),
191 default_font: String,
192 default_font_size: f64,
193 header_stmts: Vec<Statement>,
194 footer_stmts: Vec<Statement>,
195 image_objects: Vec<(usize, f64, f64)>,
196 in_header_footer: bool,
197 page_has_content: bool,
198 current_page_number: usize,
199}
200
201impl PdfCodegen {
202 pub fn new(config: &PdfConfig) -> Self {
203 let (w, h) = page_dimensions(&config.page_size);
204 let mut cg = Self {
205 objects: Vec::new(),
206 pages: Vec::new(),
207 page_width: w,
208 page_height: h,
209 margin_top: config.margins.top,
210 margin_bottom: config.margins.bottom,
211 margin_left: config.margins.left,
212 margin_right: config.margins.right,
213 cursor_y: h - config.margins.top,
214 current_stream: ContentStream::new(),
215 fonts: Vec::new(),
216 current_font: "F1".to_string(),
217 current_font_name: config.default_font.clone(),
218 current_font_size: config.default_font_size,
219 current_color: (0.0, 0.0, 0.0),
220 default_font: config.default_font.clone(),
221 default_font_size: config.default_font_size,
222 header_stmts: Vec::new(),
223 footer_stmts: Vec::new(),
224 image_objects: Vec::new(),
225 in_header_footer: false,
226 page_has_content: false,
227 current_page_number: 0,
228 };
229 cg.register_font(&config.default_font);
230 cg.register_font("Helvetica-Bold");
231 cg.register_font("Courier");
232 cg.register_font("Courier-Bold");
233 cg.register_font("Times-Roman");
234 cg.register_font("Times-Bold");
235 cg
236 }
237
238 fn register_font(&mut self, base_font: &str) -> String {
239 for (tag, name) in &self.fonts {
240 if name == base_font { return tag.clone(); }
241 }
242 let tag = format!("F{}", self.fonts.len() + 1);
243 self.fonts.push((tag.clone(), base_font.to_string()));
244 tag
245 }
246
247 fn font_tag(&self, base_font: &str) -> String {
248 for (tag, name) in &self.fonts {
249 if name == base_font { return tag.clone(); }
250 }
251 "F1".to_string()
252 }
253
254 fn content_width(&self) -> f64 {
255 self.page_width - self.margin_left - self.margin_right
256 }
257
258 fn available_height(&self) -> f64 {
259 self.cursor_y - self.margin_bottom
260 }
261
262 fn check_page_break(&mut self, needed: f64) {
263 if !self.in_header_footer && self.cursor_y - needed < self.margin_bottom {
264 self.finalize_page();
265 self.start_new_page();
266 }
267 }
268
269 fn start_new_page(&mut self) {
270 self.current_page_number += 1;
271 self.cursor_y = self.page_height - self.margin_top;
272 self.current_stream = ContentStream::new();
273 self.page_has_content = false;
274
275 if !self.header_stmts.is_empty() && !self.in_header_footer {
277 self.in_header_footer = true;
278 let saved = self.cursor_y;
279 self.cursor_y = self.page_height - 30.0;
281 let stmts = self.header_stmts.clone();
282 for s in &stmts { self.emit_statement(s); }
283 self.cursor_y = saved;
284 self.in_header_footer = false;
285 }
286 }
287
288 fn finalize_page(&mut self) {
289 if !self.footer_stmts.is_empty() && !self.in_header_footer {
291 self.in_header_footer = true;
292 let saved = self.cursor_y;
293 self.cursor_y = self.margin_bottom - 6.0;
298 let stmts = self.footer_stmts.clone();
299 for s in &stmts { self.emit_statement(s); }
300 self.cursor_y = saved;
301 self.in_header_footer = false;
302 }
303
304 let stream_data = std::mem::replace(&mut self.current_stream, ContentStream::new());
305 if stream_data.bytes().is_empty() && !self.page_has_content {
306 return;
307 }
308 let content_id = self.add_stream_object(stream_data.bytes());
309 let page_data = format!(
310 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 {} {}] /Contents {} 0 R >>",
311 fmt_f64(self.page_width), fmt_f64(self.page_height), content_id
312 );
313 let page_obj_id = self.add_object(page_data.as_bytes());
314 self.pages.push(page_obj_id);
315 }
316
317 fn add_object(&mut self, data: &[u8]) -> usize {
318 let id = self.objects.len() + 1;
319 self.objects.push(PdfObj { id, data: data.to_vec() });
320 id
321 }
322
323 fn add_stream_object(&mut self, stream_data: &[u8]) -> usize {
324 let header = format!("<< /Length {} >>", stream_data.len());
325 let mut data = Vec::new();
326 data.extend_from_slice(header.as_bytes());
327 data.extend_from_slice(b"\nstream\n");
328 data.extend_from_slice(stream_data);
329 data.extend_from_slice(b"\nendstream");
330 self.add_object(&data)
331 }
332
333 fn mark_content(&mut self) { self.page_has_content = true; }
334
335 fn substitute_page_vars(&self, text: &str) -> String {
336 if !self.in_header_footer { return text.to_string(); }
337 text.replace("{page}", &self.current_page_number.to_string())
338 .replace("{pages}", "###")
339 }
340
341 fn fixup_total_pages(&mut self) {
342 let total = self.current_page_number;
343 let total_str = format!("{:<3}", total);
344 let placeholder_hex = "232323";
345 let replacement_hex: String = total_str.chars()
346 .map(|ch| format!("{:02X}", ch as u8)).collect();
347 for obj in &mut self.objects {
348 let data_str = String::from_utf8_lossy(&obj.data).to_string();
350 if data_str.contains(placeholder_hex) {
351 obj.data = data_str.replace(placeholder_hex, &replacement_hex).into_bytes();
352 }
353 }
354 }
355
356 pub fn page_count(&self) -> usize { self.pages.len() }
357
358 pub fn generate(&mut self, program: &Program) -> Vec<u8> {
361 self.start_new_page();
362 for decl in &program.declarations {
363 if let Declaration::Page(page) = decl {
364 for stmt in &page.body { self.emit_statement(stmt); }
365 }
366 }
367 self.finalize_page();
368 self.fixup_total_pages();
369 self.serialize()
370 }
371
372 fn emit_statement(&mut self, stmt: &Statement) {
375 match stmt {
376 Statement::UIElement(ui) => self.emit_ui_element(ui),
377 Statement::If(if_stmt) => {
378 for s in &if_stmt.then_body { self.emit_statement(s); }
379 }
380 Statement::For(for_stmt) => {
381 if let Expr::ListLiteral(items) = &for_stmt.iterable {
382 for _item in items {
383 for s in &for_stmt.body { self.emit_statement(s); }
384 }
385 }
386 }
387 _ => {}
388 }
389 }
390
391 fn emit_ui_element(&mut self, ui: &UIElement) {
392 let name = match &ui.component {
393 ComponentRef::BuiltIn(n) => n.clone(),
394 ComponentRef::SubComponent(p, s) => format!("{}.{}", p, s),
395 ComponentRef::UserDefined(_) => {
396 for c in &ui.children { self.emit_statement(c); }
397 return;
398 }
399 };
400 match name.as_str() {
401 "Document" => self.emit_document(ui),
402 "Section" => self.emit_section(ui),
403 "Paragraph" => self.emit_paragraph(ui),
404 "Header" => { self.header_stmts = ui.children.clone(); }
405 "Footer" => { self.footer_stmts = ui.children.clone(); }
406 "PageBreak" => { self.finalize_page(); self.start_new_page(); }
407 "Text" => self.emit_text(ui),
408 "Heading" => self.emit_heading(ui),
409 "Table" => self.emit_table(ui),
410 "Thead" | "Tbody" => { for c in &ui.children { self.emit_statement(c); } }
411 "Trow" => {}
412 "Code" => self.emit_code(ui),
413 "Blockquote"=> self.emit_blockquote(ui),
414 "Image" => self.emit_image(ui),
415 "Divider" => self.emit_divider(),
416 "Row" => self.emit_row(ui),
417 "Container" | "Column" | "Grid" | "Stack" => {
418 for c in &ui.children { self.emit_statement(c); }
419 }
420 "Spacer" => { self.cursor_y -= self.get_spacer_size(ui); }
421 "List" | "TypeList" => self.emit_list(ui),
422 "Badge" | "Tag" => self.emit_badge(ui),
423 "Alert" => self.emit_alert(ui),
424 "Progress" => self.emit_progress(ui),
425 "Card" => self.emit_card(ui),
426 "Card.Header" | "Card.Body" | "Card.Footer" => {
427 for c in &ui.children { self.emit_statement(c); }
428 }
429 "Avatar" | "Icon" => {}
430 _ => { for c in &ui.children { self.emit_statement(c); } }
431 }
432 }
433
434 fn emit_row(&mut self, ui: &UIElement) {
437 let mut justify_between = false;
438 for arg in &ui.args {
439 if let crate::parser::Arg::Named(name, value) = arg {
440 if name == "justify" {
441 if let Expr::Identifier(v) = value {
442 if v == "between" { justify_between = true; }
443 }
444 }
445 }
446 }
447
448 if justify_between && ui.children.len() >= 2 {
449 let mut items: Vec<(String, String, f64, (f64, f64, f64))> = Vec::new();
450 for c in &ui.children {
451 if let Statement::UIElement(cu) = c {
452 let t = self.extract_text_content(cu);
453 let (f, s, col, _) = self.extract_text_style(cu);
454 items.push((t, f, s, col));
455 }
456 }
457 if items.len() >= 2 {
458 let lh = items.iter().map(|(_, _, s, _)| *s * 1.5).fold(0.0f64, f64::max);
459 self.check_page_break(lh);
460
461 let (ref t, ref f, sz, col) = items[0];
463 if !t.is_empty() {
464 let ft = self.font_tag(f);
465 self.current_stream.set_color(col.0, col.1, col.2);
466 self.current_stream.text_at(self.margin_left, self.cursor_y, &ft, sz, t);
467 }
468 let last = items.len() - 1;
470 let (ref t, ref f, sz, col) = items[last];
471 if !t.is_empty() {
472 let ft = self.font_tag(f);
473 let tw = text_width(t, f, sz);
474 let x = self.page_width - self.margin_right - tw;
475 self.current_stream.set_color(col.0, col.1, col.2);
476 self.current_stream.text_at(x, self.cursor_y, &ft, sz, t);
477 }
478 self.cursor_y -= lh;
479 self.mark_content();
480 return;
481 }
482 }
483 for c in &ui.children { self.emit_statement(c); }
484 }
485
486 fn emit_document(&mut self, ui: &UIElement) {
489 for arg in &ui.args {
490 if let crate::parser::Arg::Named(name, value) = arg {
491 if name == "page_size" || name == "pageSize" {
492 if let Expr::StringLiteral(s) = value {
493 let (w, h) = page_dimensions(s);
494 self.page_width = w;
495 self.page_height = h;
496 self.cursor_y = h - self.margin_top;
497 }
498 }
499 }
500 }
501 for c in &ui.children { self.emit_statement(c); }
502 }
503
504 fn emit_section(&mut self, ui: &UIElement) {
505 self.cursor_y -= 8.0;
506 for c in &ui.children { self.emit_statement(c); }
507 self.cursor_y -= 4.0;
508 }
509
510 fn emit_paragraph(&mut self, ui: &UIElement) {
511 let (font, size, color, align) = self.extract_text_style(ui);
512 for c in &ui.children {
513 if let Statement::UIElement(tu) = c {
514 let text = self.extract_text_content(tu);
515 if !text.is_empty() {
516 let (cf, cs, cc, ca) = self.extract_text_style(tu);
517 let f = if cf != self.default_font { &cf } else { &font };
518 let s = if (cs - self.default_font_size).abs() > 0.1 { cs } else { size };
519 let c = if cc != (0.0, 0.0, 0.0) { cc } else { color };
520 let a = if !ca.is_empty() { &ca } else { &align };
521 self.render_wrapped_text(&text, f, s, c, a);
522 }
523 }
524 }
525 self.cursor_y -= self.current_font_size * 0.5;
526 }
527
528 fn emit_text(&mut self, ui: &UIElement) {
531 let text = self.extract_text_content(ui);
532 if text.is_empty() { return; }
533 let (font, size, color, align) = self.extract_text_style(ui);
534 self.render_wrapped_text(&text, &font, size, color, &align);
535 }
536
537 fn emit_heading(&mut self, ui: &UIElement) {
540 let text = self.extract_text_content(ui);
541 if text.is_empty() { return; }
542
543 let mut level = 2;
544 let mut color = (0.0, 0.0, 0.0);
545 for m in &ui.modifiers {
546 match m.as_str() {
547 "h1" => level = 1, "h2" => level = 2, "h3" => level = 3,
548 "h4" => level = 4, "h5" => level = 5, "h6" => level = 6,
549 "primary" => color = (0.098, 0.098, 0.647),
550 "muted" => color = (0.4, 0.4, 0.4),
551 _ => {}
552 }
553 }
554 let size = match level { 1=>28.0, 2=>22.0, 3=>18.0, 4=>16.0, 5=>14.0, _=>12.0 };
555 let font = "Helvetica-Bold".to_string();
556 let (_, _, sc, align) = self.extract_text_style(ui);
557 if sc != (0.0, 0.0, 0.0) { color = sc; }
558
559 self.cursor_y -= size * 0.4;
560 self.check_page_break(size * 1.5);
561 self.render_wrapped_text(&text, &font, size, color, &align);
562 self.cursor_y -= size * 0.3;
563 }
564
565 fn emit_divider(&mut self) {
573 let total_gap = 20.0;
574 let half = total_gap / 2.0;
575
576 self.check_page_break(total_gap);
577
578 let line_y = self.cursor_y - half;
580 self.current_stream.set_stroke_color(0.80, 0.80, 0.80);
581 self.current_stream.set_line_width(0.5);
582 self.current_stream.line(self.margin_left, line_y, self.page_width - self.margin_right, line_y);
583
584 self.cursor_y -= total_gap;
585 self.mark_content();
586 }
587
588 fn emit_table(&mut self, ui: &UIElement) {
591 let mut rows: Vec<Vec<String>> = Vec::new();
592 let mut is_header: Vec<bool> = Vec::new();
593
594 for child in &ui.children {
595 if let Statement::UIElement(section) = child {
596 let sn = match §ion.component {
597 ComponentRef::BuiltIn(n) => n.clone(), _ => String::new(),
598 };
599 let hdr = sn == "Thead";
600 for rs in §ion.children {
601 if let Statement::UIElement(row) = rs {
602 let cells: Vec<String> = row.children.iter().filter_map(|c| {
603 if let Statement::UIElement(cell) = c { Some(self.extract_text_content(cell)) } else { None }
604 }).collect();
605 if !cells.is_empty() { rows.push(cells); is_header.push(hdr); }
606 }
607 }
608 }
609 }
610 if rows.is_empty() { return; }
611
612 let num_cols = rows.iter().map(|r| r.len()).max().unwrap_or(1);
613 let cw = self.content_width();
614 let col_w = cw / num_cols as f64;
615 let cell_pad = 4.0;
616 let cell_tw = col_w - cell_pad * 2.0;
617 let fs = self.current_font_size;
618 let lh = fs * 1.5;
619
620 let mut row_heights: Vec<f64> = Vec::new();
621 for row in &rows {
622 let mut mx = 1usize;
623 for ct in row {
624 let l = self.count_wrapped_lines(ct, &self.default_font.clone(), fs, cell_tw);
625 if l > mx { mx = l; }
626 }
627 row_heights.push(((mx as f64 * lh) + cell_pad * 2.0).max(fs * 2.0));
628 }
629
630 let init_h: f64 = row_heights.iter().take(2).sum();
631 self.check_page_break(init_h);
632 let table_x = self.margin_left;
633 self.mark_content();
634 self.current_stream.set_line_width(0.5);
635
636 for (ri, row) in rows.iter().enumerate() {
637 let rh = row_heights[ri];
638 if !self.in_header_footer && self.cursor_y - rh < self.margin_bottom {
639 self.finalize_page();
640 self.start_new_page();
641 }
642 let top = self.cursor_y;
643 let bot = top - rh;
644
645 if is_header[ri] {
646 self.current_stream.set_color(0.93, 0.93, 0.93);
647 self.current_stream.rect(table_x, bot, cw, rh);
648 self.current_stream.fill();
649 }
650 self.current_stream.set_stroke_color(0.75, 0.75, 0.75);
651 self.current_stream.rect(table_x, bot, cw, rh);
652 self.current_stream.stroke();
653
654 let fn_ = if is_header[ri] { "Helvetica-Bold".to_string() } else { self.default_font.clone() };
655 let ft = self.font_tag(&fn_);
656 for (ci, ct) in row.iter().enumerate() {
657 let cx = table_x + ci as f64 * col_w;
658 if ci > 0 {
659 self.current_stream.set_stroke_color(0.75, 0.75, 0.75);
660 self.current_stream.line(cx, top, cx, bot);
661 }
662 self.current_stream.set_color(0.0, 0.0, 0.0);
663 self.render_cell_text(ct, &fn_, &ft, fs, cx + cell_pad, top - cell_pad - fs, cell_tw, lh);
664 }
665 self.cursor_y = bot;
666 }
667 self.cursor_y -= 8.0;
668 }
669
670 fn count_wrapped_lines(&self, text: &str, font: &str, size: f64, max_w: f64) -> usize {
671 if max_w <= 0.0 || text.is_empty() { return 1; }
672 let sw = text_width(" ", font, size);
673 let mut count = 0usize;
674 for line in text.split('\n') {
675 count += 1;
676 let words: Vec<&str> = line.split_whitespace().collect();
677 if words.is_empty() { continue; }
678 let mut cw = 0.0;
679 for (i, w) in words.iter().enumerate() {
680 let ww = text_width(w, font, size);
681 let need = if i > 0 { sw + ww } else { ww };
682 if cw > 0.0 && cw + need > max_w { count += 1; cw = ww; }
683 else { cw += need; }
684 }
685 }
686 count.max(1)
687 }
688
689 fn render_cell_text(&mut self, text: &str, font: &str, font_tag: &str, size: f64,
690 x: f64, mut y: f64, max_w: f64, lh: f64) {
691 let sw = text_width(" ", font, size);
692 for line in text.split('\n') {
693 let words: Vec<&str> = line.split_whitespace().collect();
694 if words.is_empty() { y -= lh; continue; }
695 let mut cl = String::new();
696 let mut cw = 0.0;
697 for w in &words {
698 let ww = text_width(w, font, size);
699 if !cl.is_empty() && cw + sw + ww > max_w {
700 self.current_stream.text_at(x, y, font_tag, size, &cl);
701 y -= lh; cl = w.to_string(); cw = ww;
702 } else {
703 if !cl.is_empty() { cl.push(' '); cw += sw; }
704 cl.push_str(w); cw += ww;
705 }
706 }
707 if !cl.is_empty() {
708 if cw > max_w { cl = truncate_text(&cl, font, size, max_w); }
709 self.current_stream.text_at(x, y, font_tag, size, &cl);
710 y -= lh;
711 }
712 }
713 }
714
715 fn emit_list(&mut self, ui: &UIElement) {
722 let ordered = ui.modifiers.iter().any(|m| m == "ordered");
723 let fs = self.current_font_size;
724 let lh = fs * 1.5;
725 let df = self.default_font.clone();
726 let ft = self.font_tag(&df);
727 let indent = 16.0;
728
729 for (idx, child) in ui.children.iter().enumerate() {
730 let item_opt = match child {
732 Statement::UIElement(u) => Some(u),
733 _ => None,
734 };
735 if let Some(item) = item_opt {
736 let text = self.extract_text_content(item);
737 if text.is_empty() { continue; }
738
739 self.check_page_break(lh);
740
741 let marker = if ordered { format!("{}.", idx + 1) } else { "-".to_string() };
743 let marker_font = self.font_tag(&df);
744 self.current_stream.set_color(0.35, 0.35, 0.35);
745 self.current_stream.text_at(self.margin_left, self.cursor_y, &marker_font, fs, &marker);
746
747 let saved_ml = self.margin_left;
749 self.margin_left += indent;
750 self.render_wrapped_text(&text, &df, fs, (0.1, 0.1, 0.1), "");
751 self.margin_left = saved_ml;
752
753 self.mark_content();
754 }
755 }
756 self.cursor_y -= 4.0;
757 }
758
759 fn emit_code(&mut self, ui: &UIElement) {
762 let text = self.extract_text_content(ui);
763 if text.is_empty() { return; }
764 let is_block = ui.modifiers.iter().any(|m| m == "block");
765
766 if is_block {
767 let cfs = 10.0;
768 let clh = cfs * 1.4;
769 let pv = 12.0;
770 let ph = 8.0;
771 let lines: Vec<&str> = text.split('\n').collect();
772 let total_h = lines.len() as f64 * clh + pv * 2.0;
773
774 if total_h <= self.available_height() {
775 self.render_code_block(&lines, cfs, clh, pv, ph, total_h);
776 } else {
777 let mut rem = &lines[..];
778 while !rem.is_empty() {
779 let avail = self.available_height();
780 let max_l = ((avail - pv * 2.0) / clh).floor().max(1.0) as usize;
781 let take = rem.len().min(max_l);
782 let chunk = &rem[..take];
783 rem = &rem[take..];
784 let ch = chunk.len() as f64 * clh + pv * 2.0;
785 self.render_code_block(chunk, cfs, clh, pv, ph, ch);
786 if !rem.is_empty() { self.finalize_page(); self.start_new_page(); }
787 }
788 }
789 } else {
790 let ft = self.font_tag("Courier");
791 let lh = self.current_font_size * 1.5;
792 self.check_page_break(lh);
793 self.current_stream.set_color(0.2, 0.2, 0.2);
794 self.current_stream.text_at(self.margin_left, self.cursor_y, &ft, self.current_font_size, &text);
795 self.cursor_y -= lh;
796 self.mark_content();
797 }
798 }
799
800 fn render_code_block(&mut self, lines: &[&str], fs: f64, lh: f64, pv: f64, ph: f64, bh: f64) {
801 let ft = self.font_tag("Courier");
802 let mtw = self.content_width() - ph * 2.0;
803
804 self.current_stream.set_color(0.95, 0.95, 0.95);
806 self.current_stream.rect(self.margin_left, self.cursor_y - bh, self.content_width(), bh);
807 self.current_stream.fill();
808 self.current_stream.set_stroke_color(0.85, 0.85, 0.85);
809 self.current_stream.set_line_width(0.5);
810 self.current_stream.rect(self.margin_left, self.cursor_y - bh, self.content_width(), bh);
811 self.current_stream.stroke();
812
813 self.current_stream.set_color(0.2, 0.2, 0.2);
815 let mut y = self.cursor_y - pv - fs;
816 for line in lines {
817 let dl = truncate_text(line, "Courier", fs, mtw);
818 self.current_stream.text_at(self.margin_left + ph, y, &ft, fs, &dl);
819 y -= lh;
820 }
821 self.cursor_y -= bh + 8.0;
822 self.mark_content();
823 }
824
825 fn emit_blockquote(&mut self, ui: &UIElement) {
832 let bar_width = 3.0;
833 let bar_x = 8.0; let text_indent = 20.0; let mut texts = Vec::new();
837 for c in &ui.children {
838 if let Statement::UIElement(tu) = c {
839 let t = self.extract_text_content(tu);
840 if !t.is_empty() { texts.push(t); }
841 }
842 }
843 if texts.is_empty() { return; }
844
845 let full = texts.join("\n");
846 let font = self.default_font.clone();
847 let size = self.current_font_size;
848 let lh = size * 1.5;
849
850 let avail = self.content_width() - text_indent;
851 let lc = self.count_wrapped_lines(&full, &font, size, avail);
852 let th = lc as f64 * lh;
853 self.check_page_break(th.min(lh * 2.0));
854
855 let y_before = self.cursor_y;
857
858 let saved = self.margin_left;
860 self.margin_left += text_indent;
861 self.render_wrapped_text(&full, &font, size, (0.30, 0.30, 0.30), "");
862 self.margin_left = saved;
863
864 let y_after = self.cursor_y;
866
867 let bar_top = y_before + size * 0.75;
871 let bar_bot = y_after + lh * 0.35;
872 let bar_h = bar_top - bar_bot;
873 if bar_h > 0.0 {
874 self.current_stream.set_color(0.70, 0.70, 0.70);
875 self.current_stream.rounded_rect(
876 saved + bar_x, bar_bot, bar_width, bar_h, 1.5,
877 );
878 self.current_stream.fill();
879 }
880
881 self.cursor_y -= 8.0;
882 }
883
884 fn emit_image(&mut self, ui: &UIElement) {
887 let mut width = 200.0f64;
888 let mut height = 150.0f64;
889 for arg in &ui.args {
890 if let crate::parser::Arg::Named(name, value) = arg {
891 if let Expr::StringLiteral(_s) = value {
892 match name.as_str() { "src" => {} _ => {} }
893 }
894 }
895 }
896 if let Some(style) = &ui.style_block {
897 for sp in &style.properties {
898 match sp.name.as_str() {
899 "width" => { if let Expr::NumberLiteral(n) = sp.value { width = n; } }
900 "height" => { if let Expr::NumberLiteral(n) = sp.value { height = n; } }
901 _ => {}
902 }
903 }
904 }
905 self.check_page_break(height + 8.0);
906 self.current_stream.set_color(0.93, 0.93, 0.93);
907 self.current_stream.rect(self.margin_left, self.cursor_y - height, width, height);
908 self.current_stream.fill();
909 self.current_stream.set_stroke_color(0.8, 0.8, 0.8);
910 self.current_stream.set_line_width(1.0);
911 self.current_stream.rect(self.margin_left, self.cursor_y - height, width, height);
912 self.current_stream.stroke();
913 let ft = self.font_tag(&self.default_font.clone());
914 let lbl = "[Image]";
915 let lw = text_width(lbl, &self.default_font, 10.0);
916 self.current_stream.set_color(0.5, 0.5, 0.5);
917 self.current_stream.text_at(self.margin_left + (width - lw) / 2.0, self.cursor_y - height / 2.0, &ft, 10.0, lbl);
918 self.cursor_y -= height + 8.0;
919 self.mark_content();
920 }
921
922 fn emit_card(&mut self, ui: &UIElement) {
927 let pad = 14.0;
928 let margin = 6.0;
929 let radius = 4.0;
930 let saved_l = self.margin_left;
931 let saved_r = self.margin_right;
932
933 self.cursor_y -= margin;
934 self.margin_left += pad + margin;
935 self.margin_right += pad + margin;
936
937 let start_y = self.cursor_y;
938 let start_pages = self.pages.len();
939 self.cursor_y -= pad;
940
941 for c in &ui.children { self.emit_statement(c); }
942
943 self.cursor_y -= pad;
944 let end_y = self.cursor_y;
945
946 self.margin_left = saved_l;
947 self.margin_right = saved_r;
948
949 if self.pages.len() == start_pages {
951 let h = start_y - end_y;
952 let x = saved_l + margin;
953 let w = self.page_width - saved_l - saved_r - margin * 2.0;
954 self.current_stream.set_stroke_color(0.82, 0.82, 0.82);
955 self.current_stream.set_line_width(0.75);
956 self.current_stream.rounded_rect(x, end_y, w, h, radius);
957 self.current_stream.stroke();
958 }
959 self.cursor_y -= margin;
960 }
961
962 fn emit_badge(&mut self, ui: &UIElement) {
965 let text = self.extract_text_content(ui);
966 if text.is_empty() { return; }
967
968 let sz = 9.0;
969 let ft = self.font_tag("Helvetica-Bold");
970 let tw = text_width(&text, "Helvetica-Bold", sz);
971 let pad = 6.0;
972 let bh = sz + pad * 2.0;
973 let bw = tw + pad * 2.0;
974 let br = bh / 2.0;
975
976 self.check_page_break(bh + 4.0);
977
978 let color = self.modifier_color(&ui.modifiers);
980 let pill_y = self.cursor_y - sz - pad; self.current_stream.set_color(color.0, color.1, color.2);
982 self.current_stream.rounded_rect(self.margin_left, pill_y, bw, bh, br);
983 self.current_stream.fill();
984
985 let text_y = pill_y + pad;
987 self.current_stream.set_color(1.0, 1.0, 1.0);
988 self.current_stream.text_at(self.margin_left + pad, text_y, &ft, sz, &text);
989
990 self.cursor_y -= bh + 6.0;
991 self.mark_content();
992 }
993
994 fn emit_alert(&mut self, ui: &UIElement) {
997 let text = self.extract_text_content(ui);
998 if text.is_empty() { return; }
999
1000 let color = self.modifier_color(&ui.modifiers);
1001 let pad = 12.0;
1002 let font = self.default_font.clone();
1003 let fs = self.current_font_size;
1004 let lh = fs * 1.5;
1005 let tw_avail = self.content_width() - pad * 2.0 - 4.0;
1006 let lc = self.count_wrapped_lines(&text, &font, fs, tw_avail);
1007 let box_h = lc as f64 * lh + pad * 2.0;
1008
1009 self.check_page_break(box_h.min(lh * 3.0 + pad * 2.0));
1010
1011 let box_top = self.cursor_y;
1012 let box_bot = box_top - box_h;
1013
1014 self.current_stream.set_color(color.0 * 0.15 + 0.85, color.1 * 0.15 + 0.85, color.2 * 0.15 + 0.85);
1016 self.current_stream.rect(self.margin_left, box_bot, self.content_width(), box_h);
1017 self.current_stream.fill();
1018
1019 self.current_stream.set_color(color.0, color.1, color.2);
1021 self.current_stream.rect(self.margin_left, box_bot, 3.0, box_h);
1022 self.current_stream.fill();
1023
1024 let ft = self.font_tag(&font);
1026 let text_x = self.margin_left + pad + 4.0;
1027 let mut ty = box_top - pad - fs;
1028 let sw = text_width(" ", &font, fs);
1029 self.current_stream.set_color(0.15, 0.15, 0.15);
1030
1031 for raw_line in text.split('\n') {
1032 let words: Vec<&str> = raw_line.split_whitespace().collect();
1033 if words.is_empty() { ty -= lh; continue; }
1034 let mut cl = String::new();
1035 let mut cw = 0.0;
1036 for w in &words {
1037 let ww = text_width(w, &font, fs);
1038 if !cl.is_empty() && cw + sw + ww > tw_avail {
1039 self.current_stream.text_at(text_x, ty, &ft, fs, &cl);
1040 ty -= lh; cl = w.to_string(); cw = ww;
1041 } else {
1042 if !cl.is_empty() { cl.push(' '); cw += sw; }
1043 cl.push_str(w); cw += ww;
1044 }
1045 }
1046 if !cl.is_empty() {
1047 self.current_stream.text_at(text_x, ty, &ft, fs, &cl);
1048 ty -= lh;
1049 }
1050 }
1051
1052 self.cursor_y = box_bot - 8.0;
1053 self.mark_content();
1054 }
1055
1056 fn emit_progress(&mut self, ui: &UIElement) {
1059 let mut val = 0.0f64;
1060 let mut max = 100.0f64;
1061 for arg in &ui.args {
1062 if let crate::parser::Arg::Named(n, v) = arg {
1063 if let Expr::NumberLiteral(num) = v {
1064 match n.as_str() { "value" => val = *num, "max" => max = *num, _ => {} }
1065 }
1066 }
1067 }
1068 let bh = 8.0;
1069 let br = bh / 2.0;
1070 let frac = (val / max).min(1.0).max(0.0);
1071
1072 self.check_page_break(bh + 8.0);
1073
1074 let bar_y = self.cursor_y - bh;
1076 self.current_stream.set_color(0.9, 0.9, 0.9);
1077 self.current_stream.rounded_rect(self.margin_left, bar_y, self.content_width(), bh, br);
1078 self.current_stream.fill();
1079
1080 let fw = self.content_width() * frac;
1082 if fw > 0.5 {
1083 let color = self.modifier_color(&ui.modifiers);
1084 self.current_stream.set_color(color.0, color.1, color.2);
1085 self.current_stream.rounded_rect(self.margin_left, bar_y, fw, bh, br);
1086 self.current_stream.fill();
1087 }
1088 self.cursor_y -= bh + 8.0;
1089 self.mark_content();
1090 }
1091
1092 fn render_wrapped_text(&mut self, text: &str, font: &str, size: f64, color: (f64, f64, f64), align: &str) {
1098 let ft = self.font_tag(font);
1099 let lh = size * 1.5;
1100 let aw = self.content_width();
1101 let sw = text_width(" ", font, size);
1102
1103 for line in text.split('\n') {
1104 let words: Vec<&str> = line.split_whitespace().collect();
1105 if words.is_empty() { self.cursor_y -= lh * 0.5; continue; }
1106
1107 let mut cl = String::new();
1108 let mut cw = 0.0;
1109
1110 for w in &words {
1111 let ww = text_width(w, font, size);
1112 if !cl.is_empty() && cw + sw + ww > aw {
1113 self.check_page_break(lh);
1114 let x = self.text_x_for_align(&cl, font, size, align);
1115 self.current_stream.set_color(color.0, color.1, color.2);
1116 self.current_stream.text_at(x, self.cursor_y, &ft, size, &cl);
1117 self.cursor_y -= lh;
1118 self.mark_content();
1119 cl = w.to_string(); cw = ww;
1120 } else {
1121 if !cl.is_empty() { cl.push(' '); cw += sw; }
1122 cl.push_str(w); cw += ww;
1123 }
1124 }
1125 if !cl.is_empty() {
1126 self.check_page_break(lh);
1127 let x = self.text_x_for_align(&cl, font, size, align);
1128 self.current_stream.set_color(color.0, color.1, color.2);
1129 self.current_stream.text_at(x, self.cursor_y, &ft, size, &cl);
1130 self.cursor_y -= lh;
1131 self.mark_content();
1132 }
1133 }
1134 }
1135
1136 fn text_x_for_align(&self, text: &str, font: &str, size: f64, align: &str) -> f64 {
1137 match align {
1138 "center" => self.margin_left + (self.content_width() - text_width(text, font, size)) / 2.0,
1139 "right" => self.page_width - self.margin_right - text_width(text, font, size),
1140 _ => self.margin_left,
1141 }
1142 }
1143
1144 fn extract_text_content(&self, ui: &UIElement) -> String {
1147 for arg in &ui.args {
1148 if let crate::parser::Arg::Positional(expr) = arg {
1149 let text = self.expr_to_string(expr);
1150 return self.substitute_page_vars(&text);
1151 }
1152 }
1153 String::new()
1154 }
1155
1156 fn expr_to_string(&self, expr: &Expr) -> String {
1157 match expr {
1158 Expr::StringLiteral(s) => s.clone(),
1159 Expr::InterpolatedString(parts) => {
1160 let mut out = String::new();
1161 for p in parts {
1162 match p {
1163 StringPart::Literal(s) => out.push_str(s),
1164 StringPart::Expression(e) => out.push_str(&self.expr_to_string(e)),
1165 }
1166 }
1167 out
1168 }
1169 Expr::NumberLiteral(n) => format!("{}", n),
1170 Expr::BoolLiteral(b) => format!("{}", b),
1171 Expr::Identifier(name) => format!("{{{}}}", name),
1172 _ => String::new(),
1173 }
1174 }
1175
1176 fn extract_text_style(&self, ui: &UIElement) -> (String, f64, (f64, f64, f64), String) {
1177 let mut font = self.default_font.clone();
1178 let mut size = self.default_font_size;
1179 let mut color = (0.0, 0.0, 0.0);
1180 let mut align = String::new();
1181
1182 for m in &ui.modifiers {
1183 match m.as_str() {
1184 "bold" => font = format!("{}-Bold", font.split('-').next().unwrap_or("Helvetica")),
1185 "muted" => color = (0.4, 0.4, 0.4),
1186 "primary" => color = (0.098, 0.098, 0.647),
1187 "danger" => color = (0.86, 0.21, 0.27),
1188 "success" => color = (0.16, 0.65, 0.27),
1189 "warning" => color = (0.90, 0.56, 0.0),
1190 "info" => color = (0.0, 0.47, 0.84),
1191 "small" => size = self.default_font_size * 0.85,
1192 "large" => size = self.default_font_size * 1.25,
1193 "center" => align = "center".to_string(),
1194 "right" => align = "right".to_string(),
1195 _ => {}
1196 }
1197 }
1198 if let Some(style) = &ui.style_block {
1199 for sp in &style.properties {
1200 match sp.name.as_str() {
1201 "font-size" => { if let Expr::NumberLiteral(n) = sp.value { size = n; } }
1202 "font-family" | "font" => { if let Expr::StringLiteral(s) = &sp.value { font = s.clone(); } }
1203 "color" => { if let Expr::StringLiteral(s) = &sp.value { color = parse_color(s); } }
1204 "text-align" => { if let Expr::StringLiteral(s) = &sp.value { align = s.clone(); } }
1205 _ => {}
1206 }
1207 }
1208 }
1209 (font, size, color, align)
1210 }
1211
1212 fn modifier_color(&self, mods: &[String]) -> (f64, f64, f64) {
1213 for m in mods {
1214 match m.as_str() {
1215 "primary" => return (0.39, 0.39, 0.95),
1216 "success" => return (0.16, 0.65, 0.27),
1217 "danger" => return (0.86, 0.21, 0.27),
1218 "warning" => return (0.90, 0.56, 0.0),
1219 "info" => return (0.0, 0.47, 0.84),
1220 _ => {}
1221 }
1222 }
1223 (0.39, 0.39, 0.95)
1224 }
1225
1226 fn get_spacer_size(&self, ui: &UIElement) -> f64 {
1227 for m in &ui.modifiers {
1228 match m.as_str() {
1229 "small" | "sm" => return 8.0,
1230 "medium" | "md" => return 16.0,
1231 "large" | "lg" => return 24.0,
1232 "xl" => return 32.0,
1233 _ => {}
1234 }
1235 }
1236 16.0
1237 }
1238
1239 fn serialize(&self) -> Vec<u8> {
1242 let mut out = Vec::new();
1243 let mut offsets: Vec<usize> = Vec::new();
1244
1245 out.extend_from_slice(b"%PDF-1.7\n%\xE2\xE3\xCF\xD3\n");
1246
1247 let font_start = 3;
1248 let num_fonts = self.fonts.len();
1249 let resources_id = font_start + num_fonts;
1250
1251 let mut final_objects: Vec<(usize, Vec<u8>)> = Vec::new();
1252
1253 final_objects.push((1, b"<< /Type /Catalog /Pages 2 0 R >>".to_vec()));
1255
1256 for (i, (_, bf)) in self.fonts.iter().enumerate() {
1258 final_objects.push((font_start + i,
1259 format!("<< /Type /Font /Subtype /Type1 /BaseFont /{} /Encoding /WinAnsiEncoding >>", bf).into_bytes()));
1260 }
1261
1262 let mut fe = String::new();
1264 for (i, (tag, _)) in self.fonts.iter().enumerate() {
1265 fe.push_str(&format!("/{} {} 0 R ", tag, font_start + i));
1266 }
1267 final_objects.push((resources_id, format!("<< /Font << {} >> >>", fe).into_bytes()));
1268
1269 let mut new_page_ids: Vec<usize> = Vec::new();
1271 let mut next_id = resources_id + 1;
1272 let mut i = 0;
1273 while i + 1 < self.objects.len() {
1274 let cid = next_id;
1275 final_objects.push((cid, self.objects[i].data.clone()));
1276 next_id += 1;
1277 let pid = next_id;
1278 final_objects.push((pid, format!(
1279 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 {} {}] /Contents {} 0 R /Resources {} 0 R >>",
1280 fmt_f64(self.page_width), fmt_f64(self.page_height), cid, resources_id
1281 ).into_bytes()));
1282 new_page_ids.push(pid);
1283 next_id += 1;
1284 i += 2;
1285 }
1286
1287 let kids: Vec<String> = new_page_ids.iter().map(|id| format!("{} 0 R", id)).collect();
1289 final_objects.push((2, format!("<< /Type /Pages /Kids [{}] /Count {} >>", kids.join(" "), new_page_ids.len()).into_bytes()));
1290
1291 final_objects.sort_by_key(|(id, _)| *id);
1292
1293 let total_objects = final_objects.last().map(|(id, _)| *id).unwrap_or(0);
1294 offsets.resize(total_objects + 1, 0);
1295
1296 for (id, data) in &final_objects {
1297 offsets[*id] = out.len();
1298 out.extend_from_slice(format!("{} 0 obj\n", id).as_bytes());
1299 out.extend_from_slice(data);
1300 out.extend_from_slice(b"\nendobj\n\n");
1301 }
1302
1303 let xref_offset = out.len();
1304 out.extend_from_slice(format!("xref\n0 {}\n", total_objects + 1).as_bytes());
1305 out.extend_from_slice(b"0000000000 65535 f \n");
1306 for id in 1..=total_objects {
1307 out.extend_from_slice(format!("{:010} 00000 n \n", offsets.get(id).copied().unwrap_or(0)).as_bytes());
1308 }
1309 out.extend_from_slice(format!("trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{}\n%%EOF\n", total_objects + 1, xref_offset).as_bytes());
1310 out
1311 }
1312}
1313
1314fn fmt_f64(v: f64) -> String {
1317 if v == v.floor() { format!("{:.0}", v) } else { format!("{:.2}", v) }
1318}
1319
1320fn char_to_winansi(ch: char) -> u8 {
1321 match ch {
1322 '\u{FFFE}' => b'{', '\u{FFFF}' => b'}',
1323 '\u{2014}' => 0x97, '\u{2013}' => 0x96,
1324 '\u{2018}' => 0x91, '\u{2019}' => 0x92,
1325 '\u{201C}' => 0x93, '\u{201D}' => 0x94,
1326 '\u{2022}' => 0x95, '\u{2026}' => 0x85,
1327 '\u{2122}' => 0x99, '\u{00A9}' => 0xA9,
1328 '\u{00AE}' => 0xAE, '\u{00B0}' => 0xB0,
1329 '\u{20AC}' => 0x80,
1330 c if (c as u32) < 256 => c as u8,
1331 _ => b'?',
1332 }
1333}
1334
1335fn text_to_pdf_hex(text: &str) -> String {
1336 text.chars().map(|ch| format!("{:02X}", char_to_winansi(ch))).collect()
1337}
1338
1339fn parse_color(s: &str) -> (f64, f64, f64) {
1340 let s = s.trim_start_matches('#');
1341 if s.len() >= 6 {
1342 let r = u8::from_str_radix(&s[0..2], 16).unwrap_or(0) as f64 / 255.0;
1343 let g = u8::from_str_radix(&s[2..4], 16).unwrap_or(0) as f64 / 255.0;
1344 let b = u8::from_str_radix(&s[4..6], 16).unwrap_or(0) as f64 / 255.0;
1345 (r, g, b)
1346 } else {
1347 (0.0, 0.0, 0.0)
1348 }
1349}