pdfcrate 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1383 @@
1
+ // Ruby bindings for pdfcrate (Prawn-compatible API)
2
+
3
+ use magnus::{function, method, prelude::*, Error, RArray, RHash, Ruby, Symbol, Value};
4
+ use std::cell::RefCell;
5
+ use std::collections::HashMap;
6
+
7
+ use pdfcrate::api::layout::{
8
+ Color as RustColor, GridOptions as RustGridOptions, LayoutDocument, Margin as RustMargin,
9
+ Overflow as RustOverflow, TextAlign as RustTextAlign, TextFragment as RustTextFragment,
10
+ };
11
+ use pdfcrate::api::page::PageSize as RustPageSize;
12
+ use pdfcrate::api::Document as RustDocument;
13
+
14
+ fn rt_err(msg: impl Into<String>) -> Error {
15
+ Error::new(magnus::exception::runtime_error(), msg.into())
16
+ }
17
+
18
+ fn arg_err(msg: impl Into<String>) -> Error {
19
+ Error::new(magnus::exception::arg_error(), msg.into())
20
+ }
21
+
22
+ fn parse_hex_color(hex: &str) -> (f64, f64, f64) {
23
+ let hex = hex.trim_start_matches('#');
24
+ if hex.len() == 6 {
25
+ let r = u8::from_str_radix(&hex[0..2], 16).unwrap_or(0) as f64 / 255.0;
26
+ let g = u8::from_str_radix(&hex[2..4], 16).unwrap_or(0) as f64 / 255.0;
27
+ let b = u8::from_str_radix(&hex[4..6], 16).unwrap_or(0) as f64 / 255.0;
28
+ (r, g, b)
29
+ } else {
30
+ (0.0, 0.0, 0.0)
31
+ }
32
+ }
33
+
34
+ fn symbol_to_align(sym: &str) -> RustTextAlign {
35
+ match sym {
36
+ "left" => RustTextAlign::Left,
37
+ "center" => RustTextAlign::Center,
38
+ "right" => RustTextAlign::Right,
39
+ "justify" => RustTextAlign::Justify,
40
+ _ => RustTextAlign::Left,
41
+ }
42
+ }
43
+
44
+ fn parse_page_size(name: &str) -> RustPageSize {
45
+ match name {
46
+ "A4" | "a4" => RustPageSize::A4,
47
+ "A3" | "a3" => RustPageSize::A3,
48
+ "A5" | "a5" => RustPageSize::A5,
49
+ "LETTER" | "Letter" | "letter" => RustPageSize::Letter,
50
+ "LEGAL" | "Legal" | "legal" => RustPageSize::Legal,
51
+ _ => RustPageSize::A4,
52
+ }
53
+ }
54
+
55
+ fn extract_point(val: Value) -> Result<(f64, f64), Error> {
56
+ let arr = RArray::try_convert(val).map_err(|_| arg_err("expected [x, y] array"))?;
57
+ if arr.len() != 2 {
58
+ return Err(arg_err("expected [x, y] array with 2 elements"));
59
+ }
60
+ let x: f64 = arr.entry(0)?;
61
+ let y: f64 = arr.entry(1)?;
62
+ Ok((x, y))
63
+ }
64
+
65
+ // Helper to get optional key from RHash
66
+ fn hash_get_string(hash: &RHash, key: &str) -> Option<String> {
67
+ hash.get(Symbol::new(key))
68
+ .and_then(|v: Value| String::try_convert(v).ok())
69
+ }
70
+
71
+ fn hash_get_f64(hash: &RHash, key: &str) -> Option<f64> {
72
+ hash.get(Symbol::new(key))
73
+ .and_then(|v: Value| f64::try_convert(v).ok())
74
+ }
75
+
76
+ fn hash_get_symbol_name(hash: &RHash, key: &str) -> Option<String> {
77
+ hash.get(Symbol::new(key)).and_then(|v: Value| {
78
+ Symbol::try_convert(v)
79
+ .ok()
80
+ .and_then(|s| s.name().ok())
81
+ .map(|n| n.to_string())
82
+ })
83
+ }
84
+
85
+ enum DocumentInner {
86
+ Layout(Box<LayoutDocument>),
87
+ Consumed,
88
+ }
89
+
90
+ #[derive(Default, Clone)]
91
+ struct FontFamilyMap {
92
+ families: HashMap<String, HashMap<String, String>>,
93
+ }
94
+
95
+ impl FontFamilyMap {
96
+ fn register(&mut self, name: &str, styles: HashMap<String, String>) {
97
+ self.families.insert(name.to_string(), styles);
98
+ }
99
+
100
+ fn resolve(&self, family: &str, style: &str) -> Option<&str> {
101
+ self.families
102
+ .get(family)
103
+ .and_then(|m| m.get(style))
104
+ .map(|s| s.as_str())
105
+ }
106
+ }
107
+
108
+ #[magnus::wrap(class = "Pdfcrate::Document")]
109
+ struct Document {
110
+ inner: RefCell<DocumentInner>,
111
+ fill_color: RefCell<(f64, f64, f64)>,
112
+ stroke_color: RefCell<(f64, f64, f64)>,
113
+ line_width: RefCell<f64>,
114
+ current_font: RefCell<String>,
115
+ current_font_size: RefCell<f64>,
116
+ fill_alpha: RefCell<f64>,
117
+ stroke_alpha: RefCell<f64>,
118
+ dash_pattern: RefCell<Option<(Vec<f64>, f64)>>,
119
+ font_families: RefCell<FontFamilyMap>,
120
+ // Store page dimensions from creation time
121
+ page_width: f64,
122
+ page_height: f64,
123
+ }
124
+
125
+ impl Document {
126
+ // Pdfcrate::Document.new(page_size:, margin:, info:)
127
+ fn ruby_new(args: &[Value]) -> Result<Self, Error> {
128
+ let kwargs = if args.is_empty() {
129
+ RHash::new()
130
+ } else {
131
+ RHash::try_convert(args[0]).unwrap_or_else(|_| RHash::new())
132
+ };
133
+ // Parse page_size
134
+ let (page_size, pw, ph) = if let Some(ps_val) = kwargs.get(Symbol::new("page_size")) {
135
+ let ps_val: Value = ps_val;
136
+ if let Ok(s) = String::try_convert(ps_val) {
137
+ let ps = parse_page_size(&s);
138
+ let (w, h) = ps.dimensions(pdfcrate::api::page::PageLayout::Portrait);
139
+ (ps, w, h)
140
+ } else if let Ok(arr) = RArray::try_convert(ps_val) {
141
+ if arr.len() == 2 {
142
+ let w: f64 = arr.entry(0).unwrap_or(595.0);
143
+ let h: f64 = arr.entry(1).unwrap_or(842.0);
144
+ (RustPageSize::Custom(w, h), w, h)
145
+ } else {
146
+ (RustPageSize::A4, 595.0, 842.0)
147
+ }
148
+ } else {
149
+ (RustPageSize::A4, 595.0, 842.0)
150
+ }
151
+ } else {
152
+ (RustPageSize::A4, 595.0, 842.0)
153
+ };
154
+
155
+ // Parse margin
156
+ let margin = if let Some(m_val) = kwargs.get(Symbol::new("margin")) {
157
+ let m_val: Value = m_val;
158
+ if let Ok(n) = f64::try_convert(m_val) {
159
+ RustMargin::all(n)
160
+ } else if let Ok(arr) = RArray::try_convert(m_val) {
161
+ match arr.len() {
162
+ 1 => RustMargin::all(arr.entry::<f64>(0).unwrap_or(36.0)),
163
+ 2 => RustMargin::symmetric(
164
+ arr.entry::<f64>(0).unwrap_or(36.0),
165
+ arr.entry::<f64>(1).unwrap_or(36.0),
166
+ ),
167
+ 4 => RustMargin::new(
168
+ arr.entry::<f64>(0).unwrap_or(36.0),
169
+ arr.entry::<f64>(1).unwrap_or(36.0),
170
+ arr.entry::<f64>(2).unwrap_or(36.0),
171
+ arr.entry::<f64>(3).unwrap_or(36.0),
172
+ ),
173
+ _ => RustMargin::all(36.0),
174
+ }
175
+ } else {
176
+ RustMargin::all(36.0)
177
+ }
178
+ } else {
179
+ RustMargin::all(36.0)
180
+ };
181
+
182
+ let mut doc = RustDocument::new();
183
+ doc.page_size(page_size);
184
+
185
+ // Apply info metadata
186
+ if let Some(info_val) = kwargs.get(Symbol::new("info")) {
187
+ let info_val: Value = info_val;
188
+ if let Ok(info) = RHash::try_convert(info_val) {
189
+ if let Some(title) = hash_get_string(&info, "Title") {
190
+ doc.title(&title);
191
+ }
192
+ if let Some(author) = hash_get_string(&info, "Author") {
193
+ doc.author(&author);
194
+ }
195
+ }
196
+ }
197
+
198
+ let layout = LayoutDocument::with_margin(doc, margin);
199
+
200
+ Ok(Self {
201
+ inner: RefCell::new(DocumentInner::Layout(Box::new(layout))),
202
+ fill_color: RefCell::new((0.0, 0.0, 0.0)),
203
+ stroke_color: RefCell::new((0.0, 0.0, 0.0)),
204
+ line_width: RefCell::new(1.0),
205
+ current_font: RefCell::new("Helvetica".to_string()),
206
+ current_font_size: RefCell::new(12.0),
207
+ fill_alpha: RefCell::new(1.0),
208
+ stroke_alpha: RefCell::new(1.0),
209
+ dash_pattern: RefCell::new(None),
210
+ font_families: RefCell::new(FontFamilyMap::default()),
211
+ page_width: pw,
212
+ page_height: ph,
213
+ })
214
+ }
215
+
216
+ fn with_doc<F, R>(&self, f: F) -> Result<R, Error>
217
+ where
218
+ F: FnOnce(&mut RustDocument) -> R,
219
+ {
220
+ let mut guard = self.inner.borrow_mut();
221
+ match &mut *guard {
222
+ DocumentInner::Layout(layout) => Ok(f(layout.inner_mut())),
223
+ DocumentInner::Consumed => Err(rt_err("Document already consumed")),
224
+ }
225
+ }
226
+
227
+ fn with_layout<F, R>(&self, f: F) -> Result<R, Error>
228
+ where
229
+ F: FnOnce(&mut LayoutDocument) -> R,
230
+ {
231
+ let mut guard = self.inner.borrow_mut();
232
+ match &mut *guard {
233
+ DocumentInner::Layout(layout) => Ok(f(layout)),
234
+ DocumentInner::Consumed => Err(rt_err("Document already consumed")),
235
+ }
236
+ }
237
+
238
+ fn with_layout_ref<F, R>(&self, f: F) -> Result<R, Error>
239
+ where
240
+ F: FnOnce(&LayoutDocument) -> R,
241
+ {
242
+ let guard = self.inner.borrow();
243
+ match &*guard {
244
+ DocumentInner::Layout(layout) => Ok(f(layout)),
245
+ DocumentInner::Consumed => Err(rt_err("Document already consumed")),
246
+ }
247
+ }
248
+
249
+ /// Returns the current bounding box offset (left, bottom) for coordinate translation.
250
+ /// Inside a `canvas` block, this returns (0, 0).
251
+ fn bounds_offset(&self) -> Result<(f64, f64), Error> {
252
+ self.with_layout_ref(|layout| {
253
+ let b = layout.bounds();
254
+ (b.absolute_left(), b.absolute_bottom())
255
+ })
256
+ }
257
+
258
+ fn draw_shape<F>(&self, f: F) -> Result<(), Error>
259
+ where
260
+ F: FnOnce(&mut RustDocument),
261
+ {
262
+ let fa = *self.fill_alpha.borrow();
263
+ let sa = *self.stroke_alpha.borrow();
264
+ self.with_doc(|doc| {
265
+ if fa < 1.0 || sa < 1.0 {
266
+ doc.transparent(fa, sa, f);
267
+ } else {
268
+ f(doc);
269
+ }
270
+ })
271
+ }
272
+
273
+ fn resolve_font(&self, name: &str, style: &str) -> String {
274
+ let families = self.font_families.borrow();
275
+ if let Some(path) = families.resolve(name, style) {
276
+ return path.to_string();
277
+ }
278
+ match (name, style) {
279
+ ("Helvetica", "bold") => "Helvetica-Bold".to_string(),
280
+ ("Helvetica", "italic") => "Helvetica-Oblique".to_string(),
281
+ ("Helvetica", "bold_italic") => "Helvetica-BoldOblique".to_string(),
282
+ ("Times-Roman" | "Times", "bold") => "Times-Bold".to_string(),
283
+ ("Times-Roman" | "Times", "italic") => "Times-Italic".to_string(),
284
+ ("Times-Roman" | "Times", "bold_italic") => "Times-BoldItalic".to_string(),
285
+ ("Courier", "bold") => "Courier-Bold".to_string(),
286
+ ("Courier", "italic") => "Courier-Oblique".to_string(),
287
+ ("Courier", "bold_italic") => "Courier-BoldOblique".to_string(),
288
+ _ => name.to_string(),
289
+ }
290
+ }
291
+
292
+ fn apply_font(&self) -> Result<(), Error> {
293
+ let font_name = self.current_font.borrow().clone();
294
+ let font_size = *self.current_font_size.borrow();
295
+ self.with_doc(|doc| {
296
+ doc.font(&font_name).size(font_size);
297
+ })
298
+ }
299
+
300
+ // save_as(path)
301
+ fn save_as(&self, path: String) -> Result<(), Error> {
302
+ let bytes = self.render_bytes()?;
303
+ std::fs::write(&path, bytes).map_err(|e| rt_err(format!("Failed to write file: {}", e)))?;
304
+ Ok(())
305
+ }
306
+
307
+ fn render_bytes(&self) -> Result<Vec<u8>, Error> {
308
+ let mut guard = self.inner.borrow_mut();
309
+ let inner = std::mem::replace(&mut *guard, DocumentInner::Consumed);
310
+ match inner {
311
+ DocumentInner::Layout(layout) => {
312
+ let mut doc = layout.into_inner();
313
+ doc.render()
314
+ .map_err(|e| rt_err(format!("Render error: {}", e)))
315
+ }
316
+ DocumentInner::Consumed => Err(rt_err("Document already consumed")),
317
+ }
318
+ }
319
+
320
+ fn start_new_page(&self) -> Result<(), Error> {
321
+ self.with_layout(|layout| {
322
+ layout.start_new_page();
323
+ })
324
+ }
325
+
326
+ fn set_fill_color(&self, hex: String) -> Result<(), Error> {
327
+ *self.fill_color.borrow_mut() = parse_hex_color(&hex);
328
+ Ok(())
329
+ }
330
+
331
+ fn set_stroke_color(&self, hex: String) -> Result<(), Error> {
332
+ *self.stroke_color.borrow_mut() = parse_hex_color(&hex);
333
+ Ok(())
334
+ }
335
+
336
+ fn set_line_width(&self, width: f64) -> Result<(), Error> {
337
+ *self.line_width.borrow_mut() = width;
338
+ Ok(())
339
+ }
340
+
341
+ // dash(on, space: off) - accepts kwargs hash
342
+ fn set_dash(&self, on: f64, kwargs: RHash) -> Result<(), Error> {
343
+ let off = hash_get_f64(&kwargs, "space").unwrap_or(on);
344
+ *self.dash_pattern.borrow_mut() = Some((vec![on, off], 0.0));
345
+ Ok(())
346
+ }
347
+
348
+ fn undash(&self) -> Result<(), Error> {
349
+ *self.dash_pattern.borrow_mut() = None;
350
+ Ok(())
351
+ }
352
+
353
+ // font('name', size: n, style: :bold) { block }
354
+ fn set_font(&self, args: &[Value]) -> Result<Value, Error> {
355
+ let ruby = unsafe { Ruby::get_unchecked() };
356
+
357
+ if args.is_empty() {
358
+ return Err(arg_err("font requires at least 1 argument"));
359
+ }
360
+
361
+ let name =
362
+ String::try_convert(args[0]).map_err(|_| arg_err("font name must be a string"))?;
363
+
364
+ // Parse optional kwargs hash (last arg if it's a hash)
365
+ let mut size_opt: Option<f64> = None;
366
+ let mut style_str = "normal".to_string();
367
+
368
+ if args.len() > 1 {
369
+ if let Ok(hash) = RHash::try_convert(args[args.len() - 1]) {
370
+ size_opt = hash_get_f64(&hash, "size");
371
+ if let Some(s) = hash_get_symbol_name(&hash, "style") {
372
+ style_str = s;
373
+ }
374
+ }
375
+ }
376
+
377
+ let resolved = self.resolve_font(&name, &style_str);
378
+
379
+ let old_font = self.current_font.borrow().clone();
380
+ let old_size = *self.current_font_size.borrow();
381
+
382
+ *self.current_font.borrow_mut() = resolved;
383
+ if let Some(sz) = size_opt {
384
+ *self.current_font_size.borrow_mut() = sz;
385
+ }
386
+ self.apply_font()?;
387
+
388
+ let block = ruby.block_proc();
389
+ if let Ok(block) = block {
390
+ let result = block.call::<_, Value>(());
391
+ *self.current_font.borrow_mut() = old_font;
392
+ *self.current_font_size.borrow_mut() = old_size;
393
+ self.apply_font()?;
394
+ return result.map_err(|e| rt_err(format!("block error: {}", e)));
395
+ }
396
+
397
+ Ok(ruby.qnil().as_value())
398
+ }
399
+
400
+ // width_of(text) or width_of(text, size: n)
401
+ fn width_of(&self, args: &[Value]) -> Result<f64, Error> {
402
+ if args.is_empty() {
403
+ return Err(arg_err("width_of requires text argument"));
404
+ }
405
+ let text = String::try_convert(args[0]).map_err(|_| arg_err("text must be a string"))?;
406
+
407
+ let mut size_opt: Option<f64> = None;
408
+ if args.len() > 1 {
409
+ if let Ok(hash) = RHash::try_convert(args[args.len() - 1]) {
410
+ size_opt = hash_get_f64(&hash, "size");
411
+ }
412
+ }
413
+
414
+ if let Some(sz) = size_opt {
415
+ let old_size = *self.current_font_size.borrow();
416
+ *self.current_font_size.borrow_mut() = sz;
417
+ self.apply_font()?;
418
+ let width = self.with_layout_ref(|layout| layout.measure_text(&text))?;
419
+ *self.current_font_size.borrow_mut() = old_size;
420
+ self.apply_font()?;
421
+ Ok(width)
422
+ } else {
423
+ self.with_layout_ref(|layout| layout.measure_text(&text))
424
+ }
425
+ }
426
+
427
+ // draw_text text, at: [x, y]
428
+ fn draw_text(&self, args: &[Value]) -> Result<(), Error> {
429
+ if args.is_empty() {
430
+ return Err(arg_err("draw_text requires text argument"));
431
+ }
432
+ let text = String::try_convert(args[0]).map_err(|_| arg_err("text must be a string"))?;
433
+
434
+ let mut at_found = false;
435
+ if args.len() > 1 {
436
+ if let Ok(hash) = RHash::try_convert(args[args.len() - 1]) {
437
+ if let Some(at_val) = hash.get(Symbol::new("at")) {
438
+ at_found = true;
439
+ let at_val: Value = at_val;
440
+ let (x, y) = extract_point(at_val)?;
441
+ // Prawn's draw_text at: is relative to the current bounding box
442
+ self.with_layout(|layout| {
443
+ let bounds_left = layout.bounds().absolute_left();
444
+ let bounds_bottom = layout.bounds().absolute_bottom();
445
+ layout
446
+ .inner_mut()
447
+ .text_at(&text, [bounds_left + x, bounds_bottom + y]);
448
+ })?;
449
+ }
450
+ }
451
+ }
452
+
453
+ if !at_found {
454
+ return Err(arg_err("draw_text requires at: [x, y]"));
455
+ }
456
+
457
+ Ok(())
458
+ }
459
+
460
+ // text text, align: :center, leading: n
461
+ fn text_flow(&self, args: &[Value]) -> Result<(), Error> {
462
+ if args.is_empty() {
463
+ return Err(arg_err("text requires text argument"));
464
+ }
465
+ let text = String::try_convert(args[0]).map_err(|_| arg_err("text must be a string"))?;
466
+
467
+ let mut align_sym: Option<String> = None;
468
+ let mut leading_pt: Option<f64> = None;
469
+
470
+ if args.len() > 1 {
471
+ if let Ok(hash) = RHash::try_convert(args[args.len() - 1]) {
472
+ align_sym = hash_get_symbol_name(&hash, "align");
473
+ leading_pt = hash_get_f64(&hash, "leading");
474
+ }
475
+ }
476
+
477
+ self.with_layout(|layout| {
478
+ // Save and set alignment
479
+ if let Some(ref sym) = align_sym {
480
+ layout.align(symbol_to_align(sym));
481
+ }
482
+
483
+ // Save leading and convert Prawn additive leading to pdfcrate multiplicative
484
+ let old_leading = layout.line_height() / layout.font_height();
485
+ if let Some(extra) = leading_pt {
486
+ let font_h = layout.font_height();
487
+ if font_h > 0.0 {
488
+ layout.leading(1.0 + extra / font_h);
489
+ }
490
+ }
491
+
492
+ layout.text_wrap(&text);
493
+
494
+ // Restore leading
495
+ if leading_pt.is_some() {
496
+ layout.leading(old_leading);
497
+ }
498
+ // Restore alignment
499
+ if align_sym.is_some() {
500
+ layout.align(RustTextAlign::Left);
501
+ }
502
+ })?;
503
+
504
+ Ok(())
505
+ }
506
+
507
+ // text_box text, at:, width:, height:, overflow:, min_font_size:
508
+ fn text_box_method(&self, args: &[Value]) -> Result<Value, Error> {
509
+ let ruby = unsafe { Ruby::get_unchecked() };
510
+ if args.is_empty() {
511
+ return Err(arg_err("text_box requires text argument"));
512
+ }
513
+ let text = String::try_convert(args[0]).map_err(|_| arg_err("text must be a string"))?;
514
+
515
+ let mut point = [0.0f64, 0.0f64];
516
+ let mut width = 200.0f64;
517
+ let mut height = 100.0f64;
518
+ let mut overflow = RustOverflow::Truncate;
519
+
520
+ if args.len() > 1 {
521
+ if let Ok(hash) = RHash::try_convert(args[args.len() - 1]) {
522
+ if let Some(at_val) = hash.get(Symbol::new("at")) {
523
+ let at_val: Value = at_val;
524
+ let (x, y) = extract_point(at_val)?;
525
+ point = [x, y];
526
+ }
527
+ if let Some(w) = hash_get_f64(&hash, "width") {
528
+ width = w;
529
+ }
530
+ if let Some(h) = hash_get_f64(&hash, "height") {
531
+ height = h;
532
+ }
533
+ if let Some(min_sz) = hash_get_f64(&hash, "min_font_size") {
534
+ overflow = RustOverflow::ShrinkToFit(min_sz);
535
+ } else if let Some(ov_name) = hash_get_symbol_name(&hash, "overflow") {
536
+ overflow = match ov_name.as_str() {
537
+ "truncate" => RustOverflow::Truncate,
538
+ "shrink_to_fit" => RustOverflow::ShrinkToFit(6.0),
539
+ "expand" => RustOverflow::Expand,
540
+ _ => RustOverflow::Truncate,
541
+ };
542
+ }
543
+ }
544
+ }
545
+
546
+ self.with_layout(|layout| {
547
+ layout.text_box(&text, point, width, height, overflow);
548
+ })?;
549
+
550
+ Ok(ruby.qnil().as_value())
551
+ }
552
+
553
+ // formatted_text [{text:, styles:, color:, font:}, ...]
554
+ fn formatted_text(&self, fragments: RArray) -> Result<(), Error> {
555
+ let mut rust_frags = Vec::new();
556
+
557
+ for i in 0..fragments.len() {
558
+ let hash: RHash = fragments.entry(i as isize)?;
559
+
560
+ let text: String = hash
561
+ .get(Symbol::new("text"))
562
+ .and_then(|v: Value| String::try_convert(v).ok())
563
+ .unwrap_or_default();
564
+
565
+ let mut frag = RustTextFragment::new(&text);
566
+
567
+ // Parse styles array
568
+ if let Some(styles_val) = hash.get(Symbol::new("styles")) {
569
+ let styles_val: Value = styles_val;
570
+ if let Ok(styles) = RArray::try_convert(styles_val) {
571
+ for j in 0..styles.len() {
572
+ if let Ok(sym) = styles.entry::<Symbol>(j as isize) {
573
+ if let Ok(name) = sym.name() {
574
+ match name.as_ref() {
575
+ "bold" => {
576
+ frag = frag.style(pdfcrate::api::layout::FontStyle::Bold);
577
+ }
578
+ "italic" => {
579
+ frag = frag.style(pdfcrate::api::layout::FontStyle::Italic);
580
+ }
581
+ _ => {}
582
+ }
583
+ }
584
+ }
585
+ }
586
+ }
587
+ }
588
+
589
+ if let Some(color_str) = hash
590
+ .get(Symbol::new("color"))
591
+ .and_then(|v: Value| String::try_convert(v).ok())
592
+ {
593
+ let (r, g, b) = parse_hex_color(&color_str);
594
+ frag = frag.color(RustColor::rgb(r, g, b));
595
+ }
596
+
597
+ if let Some(font_name) = hash
598
+ .get(Symbol::new("font"))
599
+ .and_then(|v: Value| String::try_convert(v).ok())
600
+ {
601
+ frag = frag.font(&font_name);
602
+ }
603
+
604
+ if let Some(size) = hash
605
+ .get(Symbol::new("size"))
606
+ .and_then(|v: Value| f64::try_convert(v).ok())
607
+ {
608
+ frag = frag.size(size);
609
+ }
610
+
611
+ rust_frags.push(frag);
612
+ }
613
+
614
+ self.with_layout(|layout| {
615
+ layout.formatted_text(&rust_frags);
616
+ })
617
+ }
618
+
619
+ // fill_rectangle [x,y], w, h — point is top-left (Prawn convention)
620
+ // All shape methods translate coordinates relative to current bounding box (Prawn-compatible)
621
+ fn fill_rectangle(&self, pos: Value, width: f64, height: f64) -> Result<(), Error> {
622
+ let (x, y) = extract_point(pos)?;
623
+ let (ox, oy) = self.bounds_offset()?;
624
+ let fc = *self.fill_color.borrow();
625
+ self.draw_shape(|doc| {
626
+ doc.fill(|ctx| {
627
+ ctx.color(fc).rectangle([ox + x, oy + y], width, height);
628
+ });
629
+ })
630
+ }
631
+
632
+ fn stroke_rectangle(&self, pos: Value, width: f64, height: f64) -> Result<(), Error> {
633
+ let (x, y) = extract_point(pos)?;
634
+ let (ox, oy) = self.bounds_offset()?;
635
+ let sc = *self.stroke_color.borrow();
636
+ let lw = *self.line_width.borrow();
637
+ self.draw_shape(|doc| {
638
+ doc.stroke(|ctx| {
639
+ ctx.color(sc)
640
+ .line_width(lw)
641
+ .rectangle([ox + x, oy + y], width, height);
642
+ });
643
+ })
644
+ }
645
+
646
+ fn fill_rounded_rectangle(
647
+ &self,
648
+ pos: Value,
649
+ width: f64,
650
+ height: f64,
651
+ radius: f64,
652
+ ) -> Result<(), Error> {
653
+ let (x, y) = extract_point(pos)?;
654
+ let (ox, oy) = self.bounds_offset()?;
655
+ let fc = *self.fill_color.borrow();
656
+ self.draw_shape(|doc| {
657
+ doc.fill(|ctx| {
658
+ ctx.color(fc)
659
+ .rounded_rectangle([ox + x, oy + y], width, height, radius);
660
+ });
661
+ })
662
+ }
663
+
664
+ fn stroke_rounded_rectangle(
665
+ &self,
666
+ pos: Value,
667
+ width: f64,
668
+ height: f64,
669
+ radius: f64,
670
+ ) -> Result<(), Error> {
671
+ let (x, y) = extract_point(pos)?;
672
+ let (ox, oy) = self.bounds_offset()?;
673
+ let sc = *self.stroke_color.borrow();
674
+ let lw = *self.line_width.borrow();
675
+ self.draw_shape(|doc| {
676
+ doc.stroke(|ctx| {
677
+ ctx.color(sc)
678
+ .line_width(lw)
679
+ .rounded_rectangle([ox + x, oy + y], width, height, radius);
680
+ });
681
+ })
682
+ }
683
+
684
+ fn fill_circle(&self, center: Value, radius: f64) -> Result<(), Error> {
685
+ let (cx, cy) = extract_point(center)?;
686
+ let (ox, oy) = self.bounds_offset()?;
687
+ let fc = *self.fill_color.borrow();
688
+ self.draw_shape(|doc| {
689
+ doc.fill(|ctx| {
690
+ ctx.color(fc).circle([ox + cx, oy + cy], radius);
691
+ });
692
+ })
693
+ }
694
+
695
+ fn stroke_circle(&self, center: Value, radius: f64) -> Result<(), Error> {
696
+ let (cx, cy) = extract_point(center)?;
697
+ let (ox, oy) = self.bounds_offset()?;
698
+ let sc = *self.stroke_color.borrow();
699
+ let lw = *self.line_width.borrow();
700
+ self.draw_shape(|doc| {
701
+ doc.stroke(|ctx| {
702
+ ctx.color(sc).line_width(lw).circle([ox + cx, oy + cy], radius);
703
+ });
704
+ })
705
+ }
706
+
707
+ fn fill_ellipse(&self, center: Value, rx: f64, ry: f64) -> Result<(), Error> {
708
+ let (cx, cy) = extract_point(center)?;
709
+ let (ox, oy) = self.bounds_offset()?;
710
+ let fc = *self.fill_color.borrow();
711
+ self.draw_shape(|doc| {
712
+ doc.fill(|ctx| {
713
+ ctx.color(fc).ellipse([ox + cx, oy + cy], rx, ry);
714
+ });
715
+ })
716
+ }
717
+
718
+ fn stroke_ellipse(&self, center: Value, rx: f64, ry: f64) -> Result<(), Error> {
719
+ let (cx, cy) = extract_point(center)?;
720
+ let (ox, oy) = self.bounds_offset()?;
721
+ let sc = *self.stroke_color.borrow();
722
+ let lw = *self.line_width.borrow();
723
+ self.draw_shape(|doc| {
724
+ doc.stroke(|ctx| {
725
+ ctx.color(sc).line_width(lw).ellipse([ox + cx, oy + cy], rx, ry);
726
+ });
727
+ })
728
+ }
729
+
730
+ fn fill_polygon(&self, args: &[Value]) -> Result<(), Error> {
731
+ let (ox, oy) = self.bounds_offset()?;
732
+ let pts = self.parse_points_with_offset(args, ox, oy)?;
733
+ let fc = *self.fill_color.borrow();
734
+ self.draw_shape(|doc| {
735
+ doc.fill(|ctx| {
736
+ ctx.color(fc).polygon(&pts);
737
+ });
738
+ })
739
+ }
740
+
741
+ fn stroke_polygon(&self, args: &[Value]) -> Result<(), Error> {
742
+ let (ox, oy) = self.bounds_offset()?;
743
+ let pts = self.parse_points_with_offset(args, ox, oy)?;
744
+ let sc = *self.stroke_color.borrow();
745
+ let lw = *self.line_width.borrow();
746
+ self.draw_shape(|doc| {
747
+ doc.stroke(|ctx| {
748
+ ctx.color(sc).line_width(lw).polygon(&pts);
749
+ });
750
+ })
751
+ }
752
+
753
+ fn parse_points_with_offset(
754
+ &self,
755
+ args: &[Value],
756
+ ox: f64,
757
+ oy: f64,
758
+ ) -> Result<Vec<[f64; 2]>, Error> {
759
+ let mut pts = Vec::new();
760
+ for arg in args {
761
+ let (x, y) = extract_point(*arg)?;
762
+ pts.push([ox + x, oy + y]);
763
+ }
764
+ Ok(pts)
765
+ }
766
+
767
+ // stroke_horizontal_line x1, x2, at: y
768
+ fn stroke_horizontal_line(&self, args: &[Value]) -> Result<(), Error> {
769
+ if args.len() < 2 {
770
+ return Err(arg_err("stroke_horizontal_line requires x1, x2"));
771
+ }
772
+ let x1 = f64::try_convert(args[0]).map_err(|_| arg_err("x1 must be numeric"))?;
773
+ let x2 = f64::try_convert(args[1]).map_err(|_| arg_err("x2 must be numeric"))?;
774
+
775
+ let mut y = 0.0;
776
+ if args.len() > 2 {
777
+ if let Ok(hash) = RHash::try_convert(args[args.len() - 1]) {
778
+ if let Some(at_y) = hash_get_f64(&hash, "at") {
779
+ y = at_y;
780
+ }
781
+ }
782
+ }
783
+
784
+ let (ox, oy) = self.bounds_offset()?;
785
+ let sc = *self.stroke_color.borrow();
786
+ let lw = *self.line_width.borrow();
787
+ let dash = self.dash_pattern.borrow().clone();
788
+
789
+ self.draw_shape(|doc| {
790
+ doc.stroke(|ctx| {
791
+ ctx.color(sc).line_width(lw);
792
+ if let Some((ref pattern, phase)) = dash {
793
+ ctx.dash_with_phase(pattern, phase);
794
+ }
795
+ ctx.line([ox + x1, oy + y], [ox + x2, oy + y]);
796
+ });
797
+ })
798
+ }
799
+
800
+ fn stroke_line(&self, start: Value, end_pt: Value) -> Result<(), Error> {
801
+ let (x1, y1) = extract_point(start)?;
802
+ let (x2, y2) = extract_point(end_pt)?;
803
+ let (ox, oy) = self.bounds_offset()?;
804
+ let sc = *self.stroke_color.borrow();
805
+ let lw = *self.line_width.borrow();
806
+ let dash = self.dash_pattern.borrow().clone();
807
+
808
+ self.draw_shape(|doc| {
809
+ doc.stroke(|ctx| {
810
+ ctx.color(sc).line_width(lw);
811
+ if let Some((ref pattern, phase)) = dash {
812
+ ctx.dash_with_phase(pattern, phase);
813
+ }
814
+ ctx.line([ox + x1, oy + y1], [ox + x2, oy + y2]);
815
+ });
816
+ })
817
+ }
818
+
819
+ // stroke_axis(at:, color:, step_length:)
820
+ fn stroke_axis(&self, kwargs: RHash) -> Result<(), Error> {
821
+ let at = if let Some(at_val) = kwargs.get(Symbol::new("at")) {
822
+ let at_val: Value = at_val;
823
+ extract_point(at_val)?
824
+ } else {
825
+ (20.0, 20.0)
826
+ };
827
+
828
+ let color = if let Some(hex) = hash_get_string(&kwargs, "color") {
829
+ parse_hex_color(&hex)
830
+ } else {
831
+ (0.6, 0.6, 0.6)
832
+ };
833
+
834
+ let step = hash_get_f64(&kwargs, "step_length").unwrap_or(100.0);
835
+
836
+ self.with_doc(|doc| {
837
+ doc.stroke_axis(
838
+ pdfcrate::api::AxisOptions::new()
839
+ .at(at.0, at.1)
840
+ .color(color)
841
+ .step_length(step),
842
+ );
843
+ })
844
+ }
845
+
846
+ // canvas { block }
847
+ fn canvas(&self) -> Result<Value, Error> {
848
+ let ruby = unsafe { Ruby::get_unchecked() };
849
+ let block = ruby
850
+ .block_proc()
851
+ .map_err(|_| rt_err("canvas requires a block"))?;
852
+
853
+ let old_cursor = self.with_layout_ref(|layout| layout.cursor())?;
854
+
855
+ self.with_layout(|layout| {
856
+ layout.push_bounding_box_absolute(0.0, self.page_height, self.page_width, Some(self.page_height));
857
+ })?;
858
+
859
+ let result = block.call::<_, Value>(());
860
+
861
+ self.with_layout(|layout| {
862
+ layout.pop_bounding_box(old_cursor, None);
863
+ })?;
864
+
865
+ result.map_err(|e| rt_err(format!("canvas block error: {}", e)))
866
+ }
867
+
868
+ fn cursor(&self) -> Result<f64, Error> {
869
+ self.with_layout_ref(|layout| layout.cursor())
870
+ }
871
+
872
+ fn move_cursor_to(&self, y: f64) -> Result<(), Error> {
873
+ self.with_layout(|layout| {
874
+ layout.move_cursor_to(y);
875
+ })
876
+ }
877
+
878
+ fn move_down(&self, n: f64) -> Result<(), Error> {
879
+ self.with_layout(|layout| {
880
+ layout.move_down(n);
881
+ })
882
+ }
883
+
884
+ fn bounds(&self) -> Result<BoundsProxy, Error> {
885
+ let (top, width, height) = self.with_layout_ref(|layout| {
886
+ let b = layout.bounds();
887
+ (b.height(), b.width(), b.height())
888
+ })?;
889
+ Ok(BoundsProxy { top, width, height })
890
+ }
891
+
892
+ // bounding_box([x,y], width:, height:) { block }
893
+ fn bounding_box(&self, args: &[Value]) -> Result<(), Error> {
894
+ let ruby = unsafe { Ruby::get_unchecked() };
895
+
896
+ if args.is_empty() {
897
+ return Err(arg_err("bounding_box requires position argument"));
898
+ }
899
+
900
+ let (x, y) = extract_point(args[0])?;
901
+
902
+ let mut width = 100.0f64;
903
+ let mut height: Option<f64> = None;
904
+
905
+ if args.len() > 1 {
906
+ if let Ok(hash) = RHash::try_convert(args[args.len() - 1]) {
907
+ if let Some(w) = hash_get_f64(&hash, "width") {
908
+ width = w;
909
+ }
910
+ height = hash_get_f64(&hash, "height");
911
+ }
912
+ }
913
+
914
+ let block = ruby
915
+ .block_proc()
916
+ .map_err(|_| rt_err("bounding_box requires a block"))?;
917
+
918
+ let old_cursor =
919
+ self.with_layout(|layout| layout.push_bounding_box([x, y], width, height))?;
920
+
921
+ let result = block.call::<_, Value>(());
922
+
923
+ self.with_layout(|layout| {
924
+ layout.pop_bounding_box(old_cursor, height);
925
+ })?;
926
+
927
+ result.map_err(|e| rt_err(format!("bounding_box block error: {}", e)))?;
928
+ Ok(())
929
+ }
930
+
931
+ fn stroke_bounds(&self) -> Result<(), Error> {
932
+ self.with_layout(|layout| {
933
+ layout.stroke_bounds();
934
+ })
935
+ }
936
+
937
+ // indent(n) { block }
938
+ fn indent(&self, left: f64) -> Result<Value, Error> {
939
+ let ruby = unsafe { Ruby::get_unchecked() };
940
+ let block = ruby
941
+ .block_proc()
942
+ .map_err(|_| rt_err("indent requires a block"))?;
943
+
944
+ self.with_layout(|layout| layout.push_indent(left, 0.0))?;
945
+ let result = block.call::<_, Value>(());
946
+ self.with_layout(|layout| layout.pop_indent(left, 0.0))?;
947
+
948
+ result.map_err(|e| rt_err(format!("indent block error: {}", e)))
949
+ }
950
+
951
+ // float { block }
952
+ fn float(&self) -> Result<Value, Error> {
953
+ let ruby = unsafe { Ruby::get_unchecked() };
954
+ let block = ruby
955
+ .block_proc()
956
+ .map_err(|_| rt_err("float requires a block"))?;
957
+
958
+ let (saved_cursor, saved_page) =
959
+ self.with_layout_ref(|layout| (layout.cursor(), layout.inner().page_number()))?;
960
+
961
+ let result = block.call::<_, Value>(());
962
+
963
+ self.with_layout(|layout| {
964
+ if layout.inner().page_number() != saved_page {
965
+ layout.inner_mut().go_to_page(saved_page - 1);
966
+ }
967
+ layout.set_cursor(saved_cursor);
968
+ })?;
969
+
970
+ result.map_err(|e| rt_err(format!("float block error: {}", e)))
971
+ }
972
+
973
+ // transparent(opacity) { block }
974
+ fn transparent(&self, opacity: f64) -> Result<Value, Error> {
975
+ let ruby = unsafe { Ruby::get_unchecked() };
976
+ let block = ruby
977
+ .block_proc()
978
+ .map_err(|_| rt_err("transparent requires a block"))?;
979
+
980
+ let old_fill = *self.fill_alpha.borrow();
981
+ let old_stroke = *self.stroke_alpha.borrow();
982
+ *self.fill_alpha.borrow_mut() = opacity;
983
+ *self.stroke_alpha.borrow_mut() = opacity;
984
+
985
+ let result = block.call::<_, Value>(());
986
+
987
+ *self.fill_alpha.borrow_mut() = old_fill;
988
+ *self.stroke_alpha.borrow_mut() = old_stroke;
989
+
990
+ result.map_err(|e| rt_err(format!("transparent block error: {}", e)))
991
+ }
992
+
993
+ // image path, fit: [w,h], position: :center, vposition: :center
994
+ fn image(&self, args: &[Value]) -> Result<(), Error> {
995
+ if args.is_empty() {
996
+ return Err(arg_err("image requires path argument"));
997
+ }
998
+ let path = String::try_convert(args[0]).map_err(|_| arg_err("path must be a string"))?;
999
+
1000
+ let mut fit: Option<(f64, f64)> = None;
1001
+ let mut h_center = false;
1002
+ let mut v_center = false;
1003
+
1004
+ if args.len() > 1 {
1005
+ if let Ok(hash) = RHash::try_convert(args[args.len() - 1]) {
1006
+ if let Some(fit_val) = hash.get(Symbol::new("fit")) {
1007
+ let fit_val: Value = fit_val;
1008
+ if let Ok(pt) = extract_point(fit_val) {
1009
+ fit = Some(pt);
1010
+ }
1011
+ }
1012
+ if let Some(pos) = hash.get(Symbol::new("position")) {
1013
+ if let Ok(sym) = Symbol::try_convert(pos) {
1014
+ if sym.name().map(|n| n == "center").unwrap_or(false) {
1015
+ h_center = true;
1016
+ }
1017
+ }
1018
+ }
1019
+ if let Some(vpos) = hash.get(Symbol::new("vposition")) {
1020
+ if let Ok(sym) = Symbol::try_convert(vpos) {
1021
+ if sym.name().map(|n| n == "center").unwrap_or(false) {
1022
+ v_center = true;
1023
+ }
1024
+ }
1025
+ }
1026
+ }
1027
+ }
1028
+
1029
+ let data =
1030
+ std::fs::read(&path).map_err(|e| rt_err(format!("Failed to read image: {}", e)))?;
1031
+
1032
+ self.with_layout(|layout| {
1033
+ // Copy bounds values to avoid borrow conflict with inner_mut()
1034
+ let bounds_left = layout.bounds().absolute_left();
1035
+ let bounds_bottom = layout.bounds().absolute_bottom();
1036
+ let bounds_w = layout.bounds().width();
1037
+ let bounds_h = layout.bounds().height();
1038
+ let cursor_y = bounds_bottom + layout.cursor();
1039
+
1040
+ // Step 1: embed image to get pixel dimensions (without drawing)
1041
+ let embedded = layout
1042
+ .inner_mut()
1043
+ .embed_image(data.as_slice())
1044
+ .map_err(|e| rt_err(format!("Failed to embed image: {}", e)));
1045
+ let embedded = match embedded {
1046
+ Ok(e) => e,
1047
+ Err(_) => return,
1048
+ };
1049
+
1050
+ // Step 2: calculate fitted dimensions
1051
+ let (fitted_width, fitted_height) = if let Some((max_w, max_h)) = fit {
1052
+ embedded.fit_dimensions(max_w, max_h)
1053
+ } else {
1054
+ (embedded.width as f64, embedded.height as f64)
1055
+ };
1056
+
1057
+ // Step 3: calculate position with optional centering
1058
+ let img_x = if h_center {
1059
+ bounds_left + (bounds_w - fitted_width) / 2.0
1060
+ } else {
1061
+ bounds_left
1062
+ };
1063
+ let img_y = if v_center {
1064
+ let y_offset = (bounds_h - fitted_height) / 2.0;
1065
+ bounds_bottom + y_offset
1066
+ } else {
1067
+ cursor_y - fitted_height
1068
+ };
1069
+
1070
+ let opts = pdfcrate::api::ImageOptions {
1071
+ at: Some([img_x, img_y]),
1072
+ width: Some(fitted_width),
1073
+ height: Some(fitted_height),
1074
+ ..Default::default()
1075
+ };
1076
+
1077
+ layout.inner_mut().draw_embedded_image(&embedded, opts);
1078
+ layout.move_down(fitted_height);
1079
+ })?;
1080
+
1081
+ Ok(())
1082
+ }
1083
+
1084
+ fn embed_font(&self, path: String) -> Result<String, Error> {
1085
+ self.with_doc(|doc| {
1086
+ doc.embed_font_file(&path)
1087
+ .map_err(|e| rt_err(format!("Failed to embed font: {}", e)))
1088
+ })?
1089
+ }
1090
+
1091
+ fn font_families_proxy(&self) -> Result<FontFamiliesProxy, Error> {
1092
+ Ok(FontFamiliesProxy {})
1093
+ }
1094
+
1095
+ // register_font_family(name, normal: path, bold: path, ...)
1096
+ fn register_font_family(&self, args: &[Value]) -> Result<(), Error> {
1097
+ if args.is_empty() {
1098
+ return Err(arg_err("register_font_family requires name"));
1099
+ }
1100
+ let name = String::try_convert(args[0]).map_err(|_| arg_err("name must be a string"))?;
1101
+
1102
+ let mut styles = HashMap::new();
1103
+
1104
+ if args.len() > 1 {
1105
+ if let Ok(hash) = RHash::try_convert(args[args.len() - 1]) {
1106
+ for style_name in &["normal", "bold", "italic", "light"] {
1107
+ if let Some(path) = hash_get_string(&hash, style_name) {
1108
+ let font_name = self.embed_font(path)?;
1109
+ styles.insert(style_name.to_string(), font_name);
1110
+ }
1111
+ }
1112
+ }
1113
+ }
1114
+
1115
+ self.font_families.borrow_mut().register(&name, styles);
1116
+ Ok(())
1117
+ }
1118
+
1119
+ // define_grid(columns:, rows:, gutter:)
1120
+ fn define_grid(&self, kwargs: RHash) -> Result<(), Error> {
1121
+ let columns = hash_get_f64(&kwargs, "columns").unwrap_or(4.0) as usize;
1122
+ let rows = hash_get_f64(&kwargs, "rows").unwrap_or(4.0) as usize;
1123
+ let gutter = hash_get_f64(&kwargs, "gutter").unwrap_or(10.0);
1124
+
1125
+ self.with_layout(|layout| {
1126
+ let opts = RustGridOptions::new(rows, columns).gutter(gutter);
1127
+ layout.define_grid(opts);
1128
+ })
1129
+ }
1130
+
1131
+ // grid(row, col) or grid([r1,c1], [r2,c2])
1132
+ fn grid(&self, args: &[Value]) -> Result<GridProxy, Error> {
1133
+ if args.len() == 2 {
1134
+ if let (Ok(row), Ok(col)) = (usize::try_convert(args[0]), usize::try_convert(args[1])) {
1135
+ return Ok(GridProxy::Single { row, col });
1136
+ }
1137
+ let (r1, c1) = extract_point(args[0])?;
1138
+ let (r2, c2) = extract_point(args[1])?;
1139
+ return Ok(GridProxy::Span {
1140
+ start: (r1 as usize, c1 as usize),
1141
+ end: (r2 as usize, c2 as usize),
1142
+ });
1143
+ }
1144
+ Err(arg_err(
1145
+ "grid() requires 2 arguments: (row, col) or ([r1,c1], [r2,c2])",
1146
+ ))
1147
+ }
1148
+
1149
+ // Grid cell bounding box
1150
+ fn grid_cell_bb(&self, row: usize, col: usize) -> Result<Value, Error> {
1151
+ let ruby = unsafe { Ruby::get_unchecked() };
1152
+ let block = ruby
1153
+ .block_proc()
1154
+ .map_err(|_| rt_err("bounding_box requires a block"))?;
1155
+
1156
+ let (old_cursor, height) = self.with_layout(|layout| {
1157
+ let cell = layout
1158
+ .grid(row, col)
1159
+ .ok_or_else(|| rt_err("grid cell not found; call define_grid first"));
1160
+ match cell {
1161
+ Ok(cell) => {
1162
+ let bounds = layout.bounds();
1163
+ let abs_x = bounds.absolute_left() + cell.left;
1164
+ let abs_y = bounds.absolute_top() - (bounds.height() - cell.top);
1165
+ let h = cell.height;
1166
+ let oc =
1167
+ layout.push_bounding_box_absolute(abs_x, abs_y, cell.width, Some(h));
1168
+ Ok((oc, Some(h)))
1169
+ }
1170
+ Err(e) => Err(e),
1171
+ }
1172
+ })??;
1173
+
1174
+ let result = block.call::<_, Value>(());
1175
+
1176
+ self.with_layout(|layout| {
1177
+ layout.pop_bounding_box(old_cursor, height);
1178
+ })?;
1179
+
1180
+ result.map_err(|e| rt_err(format!("grid bounding_box error: {}", e)))
1181
+ }
1182
+
1183
+ // Grid span bounding box
1184
+ fn grid_span_bb(&self, r1: usize, c1: usize, r2: usize, c2: usize) -> Result<Value, Error> {
1185
+ let ruby = unsafe { Ruby::get_unchecked() };
1186
+ let block = ruby
1187
+ .block_proc()
1188
+ .map_err(|_| rt_err("bounding_box requires a block"))?;
1189
+
1190
+ let (old_cursor, height) = self.with_layout(|layout| {
1191
+ let multi = layout
1192
+ .grid_span((r1, c1), (r2, c2))
1193
+ .ok_or_else(|| rt_err("grid span not found; call define_grid first"));
1194
+ match multi {
1195
+ Ok(multi) => {
1196
+ let bounds = layout.bounds();
1197
+ let abs_x = bounds.absolute_left() + multi.left;
1198
+ let abs_y = bounds.absolute_top() - (bounds.height() - multi.top);
1199
+ let h = multi.height;
1200
+ let oc =
1201
+ layout.push_bounding_box_absolute(abs_x, abs_y, multi.width, Some(h));
1202
+ Ok((oc, Some(h)))
1203
+ }
1204
+ Err(e) => Err(e),
1205
+ }
1206
+ })??;
1207
+
1208
+ let result = block.call::<_, Value>(());
1209
+
1210
+ self.with_layout(|layout| {
1211
+ layout.pop_bounding_box(old_cursor, height);
1212
+ })?;
1213
+
1214
+ result.map_err(|e| rt_err(format!("grid span bounding_box error: {}", e)))
1215
+ }
1216
+ }
1217
+
1218
+ #[magnus::wrap(class = "Pdfcrate::Bounds")]
1219
+ struct BoundsProxy {
1220
+ top: f64,
1221
+ width: f64,
1222
+ height: f64,
1223
+ }
1224
+
1225
+ impl BoundsProxy {
1226
+ fn top(&self) -> f64 {
1227
+ self.top
1228
+ }
1229
+ fn width(&self) -> f64 {
1230
+ self.width
1231
+ }
1232
+ fn height(&self) -> f64 {
1233
+ self.height
1234
+ }
1235
+ }
1236
+
1237
+ #[magnus::wrap(class = "Pdfcrate::FontFamilies")]
1238
+ struct FontFamiliesProxy;
1239
+
1240
+ #[magnus::wrap(class = "Pdfcrate::GridProxy")]
1241
+ enum GridProxy {
1242
+ Single {
1243
+ row: usize,
1244
+ col: usize,
1245
+ },
1246
+ Span {
1247
+ start: (usize, usize),
1248
+ end: (usize, usize),
1249
+ },
1250
+ }
1251
+
1252
+ impl GridProxy {
1253
+ fn row(&self) -> usize {
1254
+ match self {
1255
+ GridProxy::Single { row, .. } => *row,
1256
+ GridProxy::Span { start, .. } => start.0,
1257
+ }
1258
+ }
1259
+
1260
+ fn col(&self) -> usize {
1261
+ match self {
1262
+ GridProxy::Single { col, .. } => *col,
1263
+ GridProxy::Span { start, .. } => start.1,
1264
+ }
1265
+ }
1266
+
1267
+ fn is_span(&self) -> bool {
1268
+ matches!(self, GridProxy::Span { .. })
1269
+ }
1270
+
1271
+ fn end_row(&self) -> usize {
1272
+ match self {
1273
+ GridProxy::Single { row, .. } => *row,
1274
+ GridProxy::Span { end, .. } => end.0,
1275
+ }
1276
+ }
1277
+
1278
+ fn end_col(&self) -> usize {
1279
+ match self {
1280
+ GridProxy::Single { col, .. } => *col,
1281
+ GridProxy::Span { end, .. } => end.1,
1282
+ }
1283
+ }
1284
+ }
1285
+
1286
+ #[magnus::init(name = "pdfcrate")]
1287
+ fn init(ruby: &Ruby) -> Result<(), Error> {
1288
+ let module = ruby.define_module("Pdfcrate")?;
1289
+
1290
+ // Document class
1291
+ let doc_class = module.define_class("Document", ruby.class_object())?;
1292
+ doc_class.define_singleton_method("new", function!(Document::ruby_new, -1))?;
1293
+ doc_class.define_method("save_as", method!(Document::save_as, 1))?;
1294
+ doc_class.define_method("render", method!(Document::render_bytes, 0))?;
1295
+ doc_class.define_method("start_new_page", method!(Document::start_new_page, 0))?;
1296
+
1297
+ // Color/line state
1298
+ doc_class.define_method("fill_color", method!(Document::set_fill_color, 1))?;
1299
+ doc_class.define_method("stroke_color", method!(Document::set_stroke_color, 1))?;
1300
+ doc_class.define_method("line_width", method!(Document::set_line_width, 1))?;
1301
+ doc_class.define_method("dash", method!(Document::set_dash, 2))?;
1302
+ doc_class.define_method("undash", method!(Document::undash, 0))?;
1303
+
1304
+ // Drawing primitives
1305
+ doc_class.define_method("fill_rectangle", method!(Document::fill_rectangle, 3))?;
1306
+ doc_class.define_method("stroke_rectangle", method!(Document::stroke_rectangle, 3))?;
1307
+ doc_class.define_method(
1308
+ "fill_rounded_rectangle",
1309
+ method!(Document::fill_rounded_rectangle, 4),
1310
+ )?;
1311
+ doc_class.define_method(
1312
+ "stroke_rounded_rectangle",
1313
+ method!(Document::stroke_rounded_rectangle, 4),
1314
+ )?;
1315
+ doc_class.define_method("fill_circle", method!(Document::fill_circle, 2))?;
1316
+ doc_class.define_method("stroke_circle", method!(Document::stroke_circle, 2))?;
1317
+ doc_class.define_method("fill_ellipse", method!(Document::fill_ellipse, 3))?;
1318
+ doc_class.define_method("stroke_ellipse", method!(Document::stroke_ellipse, 3))?;
1319
+ doc_class.define_method("fill_polygon", method!(Document::fill_polygon, -1))?;
1320
+ doc_class.define_method("stroke_polygon", method!(Document::stroke_polygon, -1))?;
1321
+ doc_class.define_method(
1322
+ "stroke_horizontal_line",
1323
+ method!(Document::stroke_horizontal_line, -1),
1324
+ )?;
1325
+ doc_class.define_method("stroke_line", method!(Document::stroke_line, 2))?;
1326
+ doc_class.define_method("stroke_axis", method!(Document::stroke_axis, 1))?;
1327
+
1328
+ // Font
1329
+ doc_class.define_method("font", method!(Document::set_font, -1))?;
1330
+ doc_class.define_method("width_of", method!(Document::width_of, -1))?;
1331
+ doc_class.define_method("embed_font", method!(Document::embed_font, 1))?;
1332
+ doc_class.define_method(
1333
+ "_font_families_raw",
1334
+ method!(Document::font_families_proxy, 0),
1335
+ )?;
1336
+ doc_class.define_method(
1337
+ "register_font_family",
1338
+ method!(Document::register_font_family, -1),
1339
+ )?;
1340
+
1341
+ // Text
1342
+ doc_class.define_method("draw_text", method!(Document::draw_text, -1))?;
1343
+ doc_class.define_method("text", method!(Document::text_flow, -1))?;
1344
+ doc_class.define_method("text_box", method!(Document::text_box_method, -1))?;
1345
+ doc_class.define_method("formatted_text", method!(Document::formatted_text, 1))?;
1346
+
1347
+ // Layout
1348
+ doc_class.define_method("canvas", method!(Document::canvas, 0))?;
1349
+ doc_class.define_method("cursor", method!(Document::cursor, 0))?;
1350
+ doc_class.define_method("move_cursor_to", method!(Document::move_cursor_to, 1))?;
1351
+ doc_class.define_method("move_down", method!(Document::move_down, 1))?;
1352
+ doc_class.define_method("bounds", method!(Document::bounds, 0))?;
1353
+ doc_class.define_method("bounding_box", method!(Document::bounding_box, -1))?;
1354
+ doc_class.define_method("stroke_bounds", method!(Document::stroke_bounds, 0))?;
1355
+ doc_class.define_method("indent", method!(Document::indent, 1))?;
1356
+ doc_class.define_method("float", method!(Document::float, 0))?;
1357
+ doc_class.define_method("transparent", method!(Document::transparent, 1))?;
1358
+
1359
+ // Image
1360
+ doc_class.define_method("image", method!(Document::image, -1))?;
1361
+
1362
+ // Grid
1363
+ doc_class.define_method("define_grid", method!(Document::define_grid, 1))?;
1364
+ doc_class.define_method("grid", method!(Document::grid, -1))?;
1365
+ doc_class.define_method("_grid_cell_bb", method!(Document::grid_cell_bb, 2))?;
1366
+ doc_class.define_method("_grid_span_bb", method!(Document::grid_span_bb, 4))?;
1367
+
1368
+ // Bounds class
1369
+ let bounds_class = module.define_class("Bounds", ruby.class_object())?;
1370
+ bounds_class.define_method("top", method!(BoundsProxy::top, 0))?;
1371
+ bounds_class.define_method("width", method!(BoundsProxy::width, 0))?;
1372
+ bounds_class.define_method("height", method!(BoundsProxy::height, 0))?;
1373
+
1374
+ // GridProxy class
1375
+ let grid_class = module.define_class("GridProxy", ruby.class_object())?;
1376
+ grid_class.define_method("row", method!(GridProxy::row, 0))?;
1377
+ grid_class.define_method("col", method!(GridProxy::col, 0))?;
1378
+ grid_class.define_method("is_span", method!(GridProxy::is_span, 0))?;
1379
+ grid_class.define_method("end_row", method!(GridProxy::end_row, 0))?;
1380
+ grid_class.define_method("end_col", method!(GridProxy::end_col, 0))?;
1381
+
1382
+ Ok(())
1383
+ }