ironpress 1.4.4 → 1.5.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.
Files changed (4) hide show
  1. checksums.yaml +4 -4
  2. data/Cargo.toml +1 -1
  3. data/src/lib.rs +368 -2
  4. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bcb5d935ab21910f5d5a56e827c163d3d77135ffa4ea64b269230f9c9ef1a5d3
4
- data.tar.gz: f6788865920a96dffcc687ef2c46b124fbb127f599f2949838e7384b701d2574
3
+ metadata.gz: 7e8c9748228dd1f6d4f33a346d27e5ed4748088eb6e69b34c45223febee260de
4
+ data.tar.gz: cd89fd251c884d17cf37f7614efaed2f42b345184593d7ed5b291b43aa143921
5
5
  SHA512:
6
- metadata.gz: 6a7d2172eb0cc296ad0b50c006fba9d84dc0c9cb6530d46feb12a0d4274352501c3223940f6c6bb8fe843e8913acd5beecf570fdb1442240fd4770499a0f92e2
7
- data.tar.gz: 07f0ce939986b339fa7b8f4f07fc7b554aad6e06c07608f5d5ca597eb2401498abed4fe90e7e911cb39cfd9f5b484e479696f61130a532b3d23e7134ce403cf5
6
+ metadata.gz: c82f9c687b328e50f25c26daaf541f60e122cd24212288ad9fcfb6465a5ab7eba1adda9235d835cd754ddce0a7d634c1bdaf2ac062bbf73f370f41cb15166d5e
7
+ data.tar.gz: bf4a16074187b6f5c662e480cad2d5a04c6b13cece0f6e47ccb1875d88c4e6259563bedc5eba51fb7bee01910f9bab97624066b659377510bca7b011afcf76c1
data/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "ironpress-ruby"
3
- version = "1.4.4"
3
+ version = "1.5.0"
4
4
  edition = "2024"
5
5
  publish = false
6
6
 
data/src/lib.rs CHANGED
@@ -1,4 +1,12 @@
1
- use magnus::{Error, RString, Ruby, define_module, function, prelude::*};
1
+ use std::cell::RefCell;
2
+ use std::path::Path;
3
+
4
+ use magnus::{
5
+ Error, IntoValue, RString, Ruby, Value,
6
+ class, define_module, function, method,
7
+ prelude::*,
8
+ typed_data,
9
+ };
2
10
 
3
11
  fn html_to_pdf(ruby: &Ruby, html: String) -> Result<RString, Error> {
4
12
  let pdf = ironpress_core::html_to_pdf(&html)
@@ -12,10 +20,368 @@ fn markdown_to_pdf(ruby: &Ruby, markdown: String) -> Result<RString, Error> {
12
20
  Ok(ruby.str_from_slice(&pdf))
13
21
  }
14
22
 
23
+ // ---------------------------------------------------------------------------
24
+ // Inner state for the builder -- stored inside a RefCell so magnus method
25
+ // receivers (&self) can mutate it.
26
+ // ---------------------------------------------------------------------------
27
+
28
+ struct ConverterInner {
29
+ page_size: ironpress_core::types::PageSize,
30
+ margin: ironpress_core::types::Margin,
31
+ compress: Option<bool>,
32
+ jpeg_quality: Option<u8>,
33
+ auto_resize_images: Option<bool>,
34
+ image_dpi: Option<f32>,
35
+ filter_dpi: Option<f32>,
36
+ mask_dpi: Option<f32>,
37
+ background_raster_dpi: Option<f32>,
38
+ occlusion_cull: Option<bool>,
39
+ sanitize: Option<bool>,
40
+ custom_fonts: Vec<(String, Vec<u8>)>,
41
+ base_path: Option<String>,
42
+ resource_root: Option<String>,
43
+ header: Option<String>,
44
+ footer: Option<String>,
45
+ }
46
+
47
+ impl Default for ConverterInner {
48
+ fn default() -> Self {
49
+ Self {
50
+ page_size: ironpress_core::types::PageSize::default(),
51
+ margin: ironpress_core::types::Margin::default(),
52
+ compress: None,
53
+ jpeg_quality: None,
54
+ auto_resize_images: None,
55
+ image_dpi: None,
56
+ filter_dpi: None,
57
+ mask_dpi: None,
58
+ background_raster_dpi: None,
59
+ occlusion_cull: None,
60
+ sanitize: None,
61
+ custom_fonts: Vec::new(),
62
+ base_path: None,
63
+ resource_root: None,
64
+ header: None,
65
+ footer: None,
66
+ }
67
+ }
68
+ }
69
+
70
+ impl ConverterInner {
71
+ /// Build an `ironpress_core::HtmlConverter` from the accumulated state.
72
+ fn build(&self) -> ironpress_core::HtmlConverter {
73
+ let mut c = ironpress_core::HtmlConverter::new()
74
+ .page_size(self.page_size)
75
+ .margin(self.margin);
76
+
77
+ if let Some(v) = self.compress {
78
+ c = c.compress(v);
79
+ }
80
+ if let Some(v) = self.jpeg_quality {
81
+ c = c.jpeg_quality(v);
82
+ }
83
+ if let Some(v) = self.auto_resize_images {
84
+ c = c.auto_resize_images(v);
85
+ }
86
+ if let Some(v) = self.image_dpi {
87
+ c = c.image_dpi(v);
88
+ }
89
+ if let Some(v) = self.filter_dpi {
90
+ c = c.filter_dpi(v);
91
+ }
92
+ if let Some(v) = self.mask_dpi {
93
+ c = c.mask_dpi(v);
94
+ }
95
+ if let Some(v) = self.background_raster_dpi {
96
+ c = c.background_raster_dpi(v);
97
+ }
98
+ if let Some(v) = self.occlusion_cull {
99
+ c = c.occlusion_cull(v);
100
+ }
101
+ if let Some(v) = self.sanitize {
102
+ c = c.sanitize(v);
103
+ }
104
+ for (name, data) in &self.custom_fonts {
105
+ c = c.add_font(name, data.clone());
106
+ }
107
+ if let Some(ref p) = self.base_path {
108
+ c = c.base_path(Path::new(p));
109
+ }
110
+ if let Some(ref p) = self.resource_root {
111
+ c = c.resource_root(Path::new(p));
112
+ }
113
+ if let Some(ref t) = self.header {
114
+ c = c.header(t.clone());
115
+ }
116
+ if let Some(ref t) = self.footer {
117
+ c = c.footer(t.clone());
118
+ }
119
+ c
120
+ }
121
+ }
122
+
123
+ // ---------------------------------------------------------------------------
124
+ // Ruby-visible class: Ironpress::HtmlConverter
125
+ //
126
+ // All setter methods mutate the receiver and return `self` so calls can be
127
+ // chained:
128
+ //
129
+ // converter = Ironpress::HtmlConverter.new
130
+ // .page_size("Letter")
131
+ // .margin(36.0)
132
+ // .compress(true)
133
+ // pdf = converter.convert("<h1>Hello</h1>")
134
+ // ---------------------------------------------------------------------------
135
+
136
+ #[magnus::wrap(class = "Ironpress::HtmlConverter", free_immediately, size)]
137
+ struct RbHtmlConverter {
138
+ inner: RefCell<ConverterInner>,
139
+ }
140
+
141
+ // Type alias for the typed Ruby object handle that can be both dereferenced
142
+ // to &RbHtmlConverter and converted back to a Ruby Value.
143
+ type RbSelf = typed_data::Obj<RbHtmlConverter>;
144
+
145
+ // --- helpers ---------------------------------------------------------------
146
+
147
+ fn parse_page_size(ruby: &Ruby, name: &str) -> Result<ironpress_core::types::PageSize, Error> {
148
+ match name.to_lowercase().as_str() {
149
+ "a4" => Ok(ironpress_core::types::PageSize::A4),
150
+ "letter" => Ok(ironpress_core::types::PageSize::LETTER),
151
+ "legal" => Ok(ironpress_core::types::PageSize::LEGAL),
152
+ _ => Err(Error::new(
153
+ ruby.exception_arg_error(),
154
+ format!("Unknown page size: {name}"),
155
+ )),
156
+ }
157
+ }
158
+
159
+ // --- methods ---------------------------------------------------------------
160
+
161
+ impl RbHtmlConverter {
162
+ /// `Ironpress::HtmlConverter.new` -- create with defaults.
163
+ fn new() -> Self {
164
+ Self {
165
+ inner: RefCell::new(ConverterInner::default()),
166
+ }
167
+ }
168
+
169
+ /// `page_size(name)` -- set page size by name ("A4", "Letter", "Legal").
170
+ fn page_size(ruby: &Ruby, rb_self: RbSelf, name: String) -> Result<Value, Error> {
171
+ let size = parse_page_size(ruby, &name)?;
172
+ rb_self.inner.borrow_mut().page_size = size;
173
+ Ok(rb_self.into_value_with(ruby))
174
+ }
175
+
176
+ /// `margin(points)` -- set uniform margin on all sides.
177
+ fn margin(ruby: &Ruby, rb_self: RbSelf, points: f64) -> Value {
178
+ rb_self.inner.borrow_mut().margin =
179
+ ironpress_core::types::Margin::uniform(points as f32);
180
+ rb_self.into_value_with(ruby)
181
+ }
182
+
183
+ /// `margin_sides(top, right, bottom, left)` -- set per-side margins.
184
+ fn margin_sides(
185
+ ruby: &Ruby,
186
+ rb_self: RbSelf,
187
+ top: f64,
188
+ right: f64,
189
+ bottom: f64,
190
+ left: f64,
191
+ ) -> Value {
192
+ rb_self.inner.borrow_mut().margin = ironpress_core::types::Margin::new(
193
+ top as f32,
194
+ right as f32,
195
+ bottom as f32,
196
+ left as f32,
197
+ );
198
+ rb_self.into_value_with(ruby)
199
+ }
200
+
201
+ /// `compress(enabled)` -- enable/disable FlateDecode compression.
202
+ fn compress(ruby: &Ruby, rb_self: RbSelf, enabled: bool) -> Value {
203
+ rb_self.inner.borrow_mut().compress = Some(enabled);
204
+ rb_self.into_value_with(ruby)
205
+ }
206
+
207
+ /// `jpeg_quality(quality)` -- JPEG quality 0-100.
208
+ fn jpeg_quality(ruby: &Ruby, rb_self: RbSelf, quality: u8) -> Value {
209
+ rb_self.inner.borrow_mut().jpeg_quality = Some(quality);
210
+ rb_self.into_value_with(ruby)
211
+ }
212
+
213
+ /// `auto_resize_images(enabled)` -- enable/disable automatic image downscaling.
214
+ fn auto_resize_images(ruby: &Ruby, rb_self: RbSelf, enabled: bool) -> Value {
215
+ rb_self.inner.borrow_mut().auto_resize_images = Some(enabled);
216
+ rb_self.into_value_with(ruby)
217
+ }
218
+
219
+ /// `image_dpi(dpi)` -- source image resolution (min 72, default 300).
220
+ fn image_dpi(ruby: &Ruby, rb_self: RbSelf, dpi: f64) -> Value {
221
+ rb_self.inner.borrow_mut().image_dpi = Some(dpi as f32);
222
+ rb_self.into_value_with(ruby)
223
+ }
224
+
225
+ /// `filter_dpi(dpi)` -- blur/filter bitmap DPI.
226
+ fn filter_dpi(ruby: &Ruby, rb_self: RbSelf, dpi: f64) -> Value {
227
+ rb_self.inner.borrow_mut().filter_dpi = Some(dpi as f32);
228
+ rb_self.into_value_with(ruby)
229
+ }
230
+
231
+ /// `mask_dpi(dpi)` -- CSS mask-image coverage DPI.
232
+ fn mask_dpi(ruby: &Ruby, rb_self: RbSelf, dpi: f64) -> Value {
233
+ rb_self.inner.borrow_mut().mask_dpi = Some(dpi as f32);
234
+ rb_self.into_value_with(ruby)
235
+ }
236
+
237
+ /// `background_raster_dpi(dpi)` -- background bitmap DPI.
238
+ fn background_raster_dpi(ruby: &Ruby, rb_self: RbSelf, dpi: f64) -> Value {
239
+ rb_self.inner.borrow_mut().background_raster_dpi = Some(dpi as f32);
240
+ rb_self.into_value_with(ruby)
241
+ }
242
+
243
+ /// `occlusion_cull(enabled)` -- enable/disable occlusion culling.
244
+ fn occlusion_cull(ruby: &Ruby, rb_self: RbSelf, enabled: bool) -> Value {
245
+ rb_self.inner.borrow_mut().occlusion_cull = Some(enabled);
246
+ rb_self.into_value_with(ruby)
247
+ }
248
+
249
+ /// `sanitize(enabled)` -- enable/disable HTML sanitization.
250
+ fn sanitize(ruby: &Ruby, rb_self: RbSelf, enabled: bool) -> Value {
251
+ rb_self.inner.borrow_mut().sanitize = Some(enabled);
252
+ rb_self.into_value_with(ruby)
253
+ }
254
+
255
+ /// `add_font(name, ttf_data)` -- register a custom TrueType font.
256
+ fn add_font(ruby: &Ruby, rb_self: RbSelf, name: String, ttf_data: RString) -> Value {
257
+ let data = unsafe { ttf_data.as_slice() }.to_vec();
258
+ rb_self.inner.borrow_mut().custom_fonts.push((name, data));
259
+ rb_self.into_value_with(ruby)
260
+ }
261
+
262
+ /// `base_path(path)` -- base directory for resolving relative paths.
263
+ fn base_path(ruby: &Ruby, rb_self: RbSelf, path: String) -> Value {
264
+ rb_self.inner.borrow_mut().base_path = Some(path);
265
+ rb_self.into_value_with(ruby)
266
+ }
267
+
268
+ /// `resource_root(path)` -- authorized resource boundary directory.
269
+ fn resource_root(ruby: &Ruby, rb_self: RbSelf, path: String) -> Value {
270
+ rb_self.inner.borrow_mut().resource_root = Some(path);
271
+ rb_self.into_value_with(ruby)
272
+ }
273
+
274
+ /// `header(text)` -- header text rendered at the top of each page.
275
+ fn header(ruby: &Ruby, rb_self: RbSelf, text: String) -> Value {
276
+ rb_self.inner.borrow_mut().header = Some(text);
277
+ rb_self.into_value_with(ruby)
278
+ }
279
+
280
+ /// `footer(text)` -- footer text rendered at the bottom of each page.
281
+ /// Use `{page}` for current page number and `{pages}` for total count.
282
+ fn footer(ruby: &Ruby, rb_self: RbSelf, text: String) -> Value {
283
+ rb_self.inner.borrow_mut().footer = Some(text);
284
+ rb_self.into_value_with(ruby)
285
+ }
286
+
287
+ /// `convert(html)` -- convert HTML to PDF bytes.
288
+ fn convert(ruby: &Ruby, rb_self: RbSelf, html: String) -> Result<RString, Error> {
289
+ let converter = rb_self.inner.borrow().build();
290
+ let pdf = converter
291
+ .convert(&html)
292
+ .map_err(|e| Error::new(ruby.exception_runtime_error(), e.to_string()))?;
293
+ Ok(ruby.str_from_slice(&pdf))
294
+ }
295
+
296
+ /// `convert_markdown(markdown)` -- convert Markdown to PDF bytes.
297
+ fn convert_markdown(ruby: &Ruby, rb_self: RbSelf, markdown: String) -> Result<RString, Error> {
298
+ let converter = rb_self.inner.borrow().build();
299
+ let pdf = converter
300
+ .convert_markdown(&markdown)
301
+ .map_err(|e| Error::new(ruby.exception_runtime_error(), e.to_string()))?;
302
+ Ok(ruby.str_from_slice(&pdf))
303
+ }
304
+
305
+ /// `convert_to_file(html, path)` -- convert HTML and write PDF to a file.
306
+ fn convert_to_file(
307
+ ruby: &Ruby,
308
+ rb_self: RbSelf,
309
+ html: String,
310
+ path: String,
311
+ ) -> Result<Value, Error> {
312
+ let converter = rb_self.inner.borrow().build();
313
+ let pdf = converter
314
+ .convert(&html)
315
+ .map_err(|e| Error::new(ruby.exception_runtime_error(), e.to_string()))?;
316
+ std::fs::write(&path, &pdf)
317
+ .map_err(|e| Error::new(ruby.exception_runtime_error(), e.to_string()))?;
318
+ Ok(rb_self.into_value_with(ruby))
319
+ }
320
+
321
+ /// `convert_markdown_to_file(markdown, path)` -- convert Markdown and write PDF to a file.
322
+ fn convert_markdown_to_file(
323
+ ruby: &Ruby,
324
+ rb_self: RbSelf,
325
+ markdown: String,
326
+ path: String,
327
+ ) -> Result<Value, Error> {
328
+ let converter = rb_self.inner.borrow().build();
329
+ let pdf = converter
330
+ .convert_markdown(&markdown)
331
+ .map_err(|e| Error::new(ruby.exception_runtime_error(), e.to_string()))?;
332
+ std::fs::write(&path, &pdf)
333
+ .map_err(|e| Error::new(ruby.exception_runtime_error(), e.to_string()))?;
334
+ Ok(rb_self.into_value_with(ruby))
335
+ }
336
+ }
337
+
15
338
  #[magnus::init]
16
- fn init(ruby: &Ruby) -> Result<(), Error> {
339
+ fn init(_ruby: &Ruby) -> Result<(), Error> {
17
340
  let module = define_module("Ironpress")?;
341
+
342
+ // Module-level convenience functions (existing API).
18
343
  module.define_singleton_method("html_to_pdf", function!(html_to_pdf, 1))?;
19
344
  module.define_singleton_method("markdown_to_pdf", function!(markdown_to_pdf, 1))?;
345
+
346
+ // HtmlConverter class under the Ironpress module.
347
+ let klass = module.define_class("HtmlConverter", class::object())?;
348
+ klass.define_singleton_method("new", function!(RbHtmlConverter::new, 0))?;
349
+ klass.define_method("page_size", method!(RbHtmlConverter::page_size, 1))?;
350
+ klass.define_method("margin", method!(RbHtmlConverter::margin, 1))?;
351
+ klass.define_method("margin_sides", method!(RbHtmlConverter::margin_sides, 4))?;
352
+ klass.define_method("compress", method!(RbHtmlConverter::compress, 1))?;
353
+ klass.define_method("jpeg_quality", method!(RbHtmlConverter::jpeg_quality, 1))?;
354
+ klass.define_method(
355
+ "auto_resize_images",
356
+ method!(RbHtmlConverter::auto_resize_images, 1),
357
+ )?;
358
+ klass.define_method("image_dpi", method!(RbHtmlConverter::image_dpi, 1))?;
359
+ klass.define_method("filter_dpi", method!(RbHtmlConverter::filter_dpi, 1))?;
360
+ klass.define_method("mask_dpi", method!(RbHtmlConverter::mask_dpi, 1))?;
361
+ klass.define_method(
362
+ "background_raster_dpi",
363
+ method!(RbHtmlConverter::background_raster_dpi, 1),
364
+ )?;
365
+ klass.define_method("occlusion_cull", method!(RbHtmlConverter::occlusion_cull, 1))?;
366
+ klass.define_method("sanitize", method!(RbHtmlConverter::sanitize, 1))?;
367
+ klass.define_method("add_font", method!(RbHtmlConverter::add_font, 2))?;
368
+ klass.define_method("base_path", method!(RbHtmlConverter::base_path, 1))?;
369
+ klass.define_method("resource_root", method!(RbHtmlConverter::resource_root, 1))?;
370
+ klass.define_method("header", method!(RbHtmlConverter::header, 1))?;
371
+ klass.define_method("footer", method!(RbHtmlConverter::footer, 1))?;
372
+ klass.define_method("convert", method!(RbHtmlConverter::convert, 1))?;
373
+ klass.define_method(
374
+ "convert_markdown",
375
+ method!(RbHtmlConverter::convert_markdown, 1),
376
+ )?;
377
+ klass.define_method(
378
+ "convert_to_file",
379
+ method!(RbHtmlConverter::convert_to_file, 2),
380
+ )?;
381
+ klass.define_method(
382
+ "convert_markdown_to_file",
383
+ method!(RbHtmlConverter::convert_markdown_to_file, 2),
384
+ )?;
385
+
20
386
  Ok(())
21
387
  }
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ironpress
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.4
4
+ version: 1.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Paul Gaston Gouron
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-07-29 00:00:00.000000000 Z
11
+ date: 2026-08-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rb_sys