ironpress 1.5.2 → 1.5.3

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7511df07cf2fb2bcd059fefddd22aaf5b8f903ffda05be04f7f0fb92752d894d
4
- data.tar.gz: 7e02162bdaa8acad1df3fd4822d15a46da8c3099dee2892e07f23e41e08d92d8
3
+ metadata.gz: cfcd5dc3040d841312793e77a4119d49c14db383e9dcc7970182159c1989e206
4
+ data.tar.gz: e25587f9d4d4569b59cdff1da629d342bee5fc3b5e2c16832a0c5bea2809289c
5
5
  SHA512:
6
- metadata.gz: ccd9fb149b8ac0963fb1d9f560c5f1ec9d846f8a556e18ea18389008edfc99ee0599fd2a4415a64f912d8700a5d6aeb9363b85f748f4afe184e0e6c9a995a8e2
7
- data.tar.gz: 6fccbd311bfdb5812c63432d5acc93cd9a29834ecd0568eb8cbc01f171e13b6ce9912393d514ea045836b57f7005b187bc7b54204f02d6b0e8eb68e58c4735e1
6
+ metadata.gz: 1b608a05c7b569eb4282776226568d01a314f36f61d530a3db3c367622d83a11e16c1d3b915c653a9b9915bd13f99ec9fc6c0f51d88366e8b06a7bed07c6db2f
7
+ data.tar.gz: c9b9d71b037da3b8df4086e076b968075e34a10f03d56dbbc0263a6d9cdd473e3012b73320e548ea99599678c762385bececfae69d2969440cfff21425e5f99e
data/README.md CHANGED
@@ -1,35 +1,58 @@
1
- # Ironpress
1
+ # Ironpress for Ruby
2
2
 
3
- Pure Rust HTML/CSS/Markdown to PDF converter. No browser, no system dependencies.
3
+ Generate PDF bytes from HTML or Markdown without launching a browser.
4
4
 
5
5
  ## Installation
6
6
 
7
- ```ruby
8
- gem install ironpress
9
- ```
10
-
11
- Or in your Gemfile:
12
-
13
7
  ```ruby
14
8
  gem "ironpress"
15
9
  ```
16
10
 
11
+ Ironpress supports Ruby 3.0 and later. Releases include a source gem and native
12
+ gems for Linux, macOS, and Windows.
13
+
17
14
  ## Usage
18
15
 
19
16
  ```ruby
20
17
  require "ironpress"
21
18
 
22
- # HTML to PDF
23
- pdf = Ironpress.html_to_pdf("<h1>Hello World</h1><p>Generated with Ironpress.</p>")
19
+ pdf = Ironpress.html_to_pdf("<h1>Hello</h1>")
24
20
  File.binwrite("output.pdf", pdf)
21
+ ```
25
22
 
26
- # Markdown to PDF
27
- pdf = Ironpress.markdown_to_pdf("# Hello\n\nGenerated from **Markdown**.")
28
- File.binwrite("output.pdf", pdf)
23
+ Configuration methods return the converter, so policies can be composed in an
24
+ idiomatic chain:
25
+
26
+ ```ruby
27
+ converter = Ironpress::HtmlConverter.new
28
+ .page_size("Letter")
29
+ .margin_sides(36, 54, 36, 54)
30
+ .header("Quarterly report")
31
+ .footer("Page {page} of {pages}")
32
+ .sanitize(true)
33
+
34
+ pdf = converter.convert("<h1>Results</h1>")
29
35
  ```
30
36
 
31
- ## Performance
37
+ The converter supports page geometry, PDF and image quality, sanitization,
38
+ headers and footers, custom TTF fonts, and optional CJK or emoji packs. Native
39
+ Ruby also supports constrained local resources through `base_path` and
40
+ `resource_root`, plus direct file output.
41
+
42
+ See the [binding capability matrix](../README.md) for the contract shared with
43
+ Rust, Python, and WebAssembly.
44
+
45
+ ## Font packs
46
+
47
+ Ironpress never downloads fallback fonts while rendering. Download the artifact
48
+ your application needs, verify it as part of deployment, then install its bytes:
49
+
50
+ ```ruby
51
+ converter = Ironpress::HtmlConverter.new.add_font_pack(
52
+ "cjk-jp",
53
+ File.binread("ironpress-font-cjk-jp.ttf")
54
+ )
55
+ pdf = converter.convert("<p lang='ja'>日本語</p>")
56
+ ```
32
57
 
33
- - **10-100x faster** than browser-based solutions (Puppeteer, wkhtmltopdf)
34
- - **~5MB** native extension, no runtime dependencies
35
- - Instant startup, no browser process
58
+ Valid pack names are `cjk-jp`, `cjk-kr`, `cjk-sc`, `cjk-tc`, and `emoji`.
@@ -0,0 +1,17 @@
1
+ [package]
2
+ name = "ironpress-ruby"
3
+ version = "1.5.3"
4
+ edition = "2024"
5
+ publish = false
6
+ description = "Ruby bindings for the Ironpress HTML-to-PDF engine"
7
+ license = "MIT"
8
+ repository = "https://github.com/gastongouron/ironpress"
9
+
10
+ [lib]
11
+ name = "ironpress_ruby"
12
+ crate-type = ["cdylib"]
13
+
14
+ [dependencies]
15
+ ironpress-core = { version = "=1.5.3", package = "ironpress" }
16
+ magnus = "0.7"
17
+ rb-sys = { version = "0.9", default-features = false, features = ["stable-api-compiled-fallback"] }
@@ -1,2 +1,6 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "mkmf"
2
- create_makefile("ironpress/ironpress")
4
+ require "rb_sys/mkmf"
5
+
6
+ create_rust_makefile("ironpress/ironpress_ruby")
@@ -0,0 +1,238 @@
1
+ use std::path::Path;
2
+
3
+ use magnus::{Error, RString, Ruby, class, define_module, function, method, prelude::*};
4
+
5
+ fn html_to_pdf(ruby: &Ruby, html: String) -> Result<RString, Error> {
6
+ let pdf = ironpress_core::html_to_pdf(&html)
7
+ .map_err(|error| Error::new(ruby.exception_runtime_error(), error.to_string()))?;
8
+ Ok(ruby.str_from_slice(&pdf))
9
+ }
10
+
11
+ fn markdown_to_pdf(ruby: &Ruby, markdown: String) -> Result<RString, Error> {
12
+ let pdf = ironpress_core::markdown_to_pdf(&markdown)
13
+ .map_err(|error| Error::new(ruby.exception_runtime_error(), error.to_string()))?;
14
+ Ok(ruby.str_from_slice(&pdf))
15
+ }
16
+
17
+ #[magnus::wrap(class = "Ironpress::NativeConverter", free_immediately, size)]
18
+ struct NativeConverter {
19
+ converter: ironpress_core::HtmlConverter,
20
+ }
21
+
22
+ impl NativeConverter {
23
+ fn new() -> Self {
24
+ Self {
25
+ converter: ironpress_core::HtmlConverter::new(),
26
+ }
27
+ }
28
+
29
+ fn configured(
30
+ &self,
31
+ configure: impl FnOnce(ironpress_core::HtmlConverter) -> ironpress_core::HtmlConverter,
32
+ ) -> Self {
33
+ Self {
34
+ converter: configure(self.converter.clone()),
35
+ }
36
+ }
37
+
38
+ fn page_size(ruby: &Ruby, this: &Self, name: String) -> Result<Self, Error> {
39
+ let size = match name.to_ascii_lowercase().as_str() {
40
+ "a4" => ironpress_core::PageSize::A4,
41
+ "letter" => ironpress_core::PageSize::LETTER,
42
+ "legal" => ironpress_core::PageSize::LEGAL,
43
+ _ => {
44
+ return Err(Error::new(
45
+ ruby.exception_arg_error(),
46
+ format!("unknown page size `{name}`"),
47
+ ));
48
+ }
49
+ };
50
+ Ok(this.configured(|converter| converter.page_size(size)))
51
+ }
52
+
53
+ fn page_size_custom(&self, width: f64, height: f64) -> Self {
54
+ self.configured(|converter| {
55
+ converter.page_size(ironpress_core::PageSize::new(width as f32, height as f32))
56
+ })
57
+ }
58
+
59
+ fn margin(&self, points: f64) -> Self {
60
+ self.configured(|converter| {
61
+ converter.margin(ironpress_core::Margin::uniform(points as f32))
62
+ })
63
+ }
64
+
65
+ fn margin_sides(&self, top: f64, right: f64, bottom: f64, left: f64) -> Self {
66
+ self.configured(|converter| {
67
+ converter.margin(ironpress_core::Margin::new(
68
+ top as f32,
69
+ right as f32,
70
+ bottom as f32,
71
+ left as f32,
72
+ ))
73
+ })
74
+ }
75
+
76
+ fn compress(&self, enabled: bool) -> Self {
77
+ self.configured(|converter| converter.compress(enabled))
78
+ }
79
+
80
+ fn jpeg_quality(&self, quality: u8) -> Self {
81
+ self.configured(|converter| converter.jpeg_quality(quality))
82
+ }
83
+
84
+ fn auto_resize_images(&self, enabled: bool) -> Self {
85
+ self.configured(|converter| converter.auto_resize_images(enabled))
86
+ }
87
+
88
+ fn image_dpi(&self, dpi: f64) -> Self {
89
+ self.configured(|converter| converter.image_dpi(dpi as f32))
90
+ }
91
+
92
+ fn filter_dpi(&self, dpi: f64) -> Self {
93
+ self.configured(|converter| converter.filter_dpi(dpi as f32))
94
+ }
95
+
96
+ fn mask_dpi(&self, dpi: f64) -> Self {
97
+ self.configured(|converter| converter.mask_dpi(dpi as f32))
98
+ }
99
+
100
+ fn background_raster_dpi(&self, dpi: f64) -> Self {
101
+ self.configured(|converter| converter.background_raster_dpi(dpi as f32))
102
+ }
103
+
104
+ fn occlusion_cull(&self, enabled: bool) -> Self {
105
+ self.configured(|converter| converter.occlusion_cull(enabled))
106
+ }
107
+
108
+ fn sanitize(&self, enabled: bool) -> Self {
109
+ self.configured(|converter| converter.sanitize(enabled))
110
+ }
111
+
112
+ fn add_font(&self, name: String, data: RString) -> Self {
113
+ // SAFETY: `data` remains rooted for this call and is copied before returning.
114
+ let bytes = unsafe { data.as_slice() }.to_vec();
115
+ self.configured(|converter| converter.add_font(&name, bytes))
116
+ }
117
+
118
+ fn add_font_pack(ruby: &Ruby, this: &Self, kind: String, data: RString) -> Result<Self, Error> {
119
+ let kind = kind
120
+ .parse::<ironpress_core::FontPackKind>()
121
+ .map_err(|error| Error::new(ruby.exception_arg_error(), error.to_string()))?;
122
+ // SAFETY: `data` remains rooted for this call and is copied before returning.
123
+ let bytes = unsafe { data.as_slice() }.to_vec();
124
+ let pack = ironpress_core::FontPack::parse(kind, bytes)
125
+ .map_err(|error| Error::new(ruby.exception_arg_error(), error.to_string()))?;
126
+ Ok(this.configured(|converter| converter.add_font_pack(pack)))
127
+ }
128
+
129
+ fn base_path(&self, path: String) -> Self {
130
+ self.configured(|converter| converter.base_path(Path::new(&path)))
131
+ }
132
+
133
+ fn resource_root(&self, path: String) -> Self {
134
+ self.configured(|converter| converter.resource_root(Path::new(&path)))
135
+ }
136
+
137
+ fn header(&self, text: String) -> Self {
138
+ self.configured(|converter| converter.header(text))
139
+ }
140
+
141
+ fn footer(&self, text: String) -> Self {
142
+ self.configured(|converter| converter.footer(text))
143
+ }
144
+
145
+ fn convert(ruby: &Ruby, this: &Self, html: String) -> Result<RString, Error> {
146
+ let pdf = this
147
+ .converter
148
+ .convert(&html)
149
+ .map_err(|error| Error::new(ruby.exception_runtime_error(), error.to_string()))?;
150
+ Ok(ruby.str_from_slice(&pdf))
151
+ }
152
+
153
+ fn convert_markdown(ruby: &Ruby, this: &Self, markdown: String) -> Result<RString, Error> {
154
+ let pdf = this
155
+ .converter
156
+ .convert_markdown(&markdown)
157
+ .map_err(|error| Error::new(ruby.exception_runtime_error(), error.to_string()))?;
158
+ Ok(ruby.str_from_slice(&pdf))
159
+ }
160
+
161
+ fn convert_to_file(ruby: &Ruby, this: &Self, html: String, path: String) -> Result<(), Error> {
162
+ let pdf = this
163
+ .converter
164
+ .convert(&html)
165
+ .map_err(|error| Error::new(ruby.exception_runtime_error(), error.to_string()))?;
166
+ std::fs::write(path, pdf)
167
+ .map_err(|error| Error::new(ruby.exception_runtime_error(), error.to_string()))
168
+ }
169
+
170
+ fn convert_markdown_to_file(
171
+ ruby: &Ruby,
172
+ this: &Self,
173
+ markdown: String,
174
+ path: String,
175
+ ) -> Result<(), Error> {
176
+ let pdf = this
177
+ .converter
178
+ .convert_markdown(&markdown)
179
+ .map_err(|error| Error::new(ruby.exception_runtime_error(), error.to_string()))?;
180
+ std::fs::write(path, pdf)
181
+ .map_err(|error| Error::new(ruby.exception_runtime_error(), error.to_string()))
182
+ }
183
+ }
184
+
185
+ #[magnus::init]
186
+ fn init(_ruby: &Ruby) -> Result<(), Error> {
187
+ let module = define_module("Ironpress")?;
188
+ module.define_singleton_method("html_to_pdf", function!(html_to_pdf, 1))?;
189
+ module.define_singleton_method("markdown_to_pdf", function!(markdown_to_pdf, 1))?;
190
+
191
+ let class = module.define_class("NativeConverter", class::object())?;
192
+ class.define_singleton_method("new", function!(NativeConverter::new, 0))?;
193
+ class.define_method("page_size", method!(NativeConverter::page_size, 1))?;
194
+ class.define_method(
195
+ "page_size_custom",
196
+ method!(NativeConverter::page_size_custom, 2),
197
+ )?;
198
+ class.define_method("margin", method!(NativeConverter::margin, 1))?;
199
+ class.define_method("margin_sides", method!(NativeConverter::margin_sides, 4))?;
200
+ class.define_method("compress", method!(NativeConverter::compress, 1))?;
201
+ class.define_method("jpeg_quality", method!(NativeConverter::jpeg_quality, 1))?;
202
+ class.define_method(
203
+ "auto_resize_images",
204
+ method!(NativeConverter::auto_resize_images, 1),
205
+ )?;
206
+ class.define_method("image_dpi", method!(NativeConverter::image_dpi, 1))?;
207
+ class.define_method("filter_dpi", method!(NativeConverter::filter_dpi, 1))?;
208
+ class.define_method("mask_dpi", method!(NativeConverter::mask_dpi, 1))?;
209
+ class.define_method(
210
+ "background_raster_dpi",
211
+ method!(NativeConverter::background_raster_dpi, 1),
212
+ )?;
213
+ class.define_method(
214
+ "occlusion_cull",
215
+ method!(NativeConverter::occlusion_cull, 1),
216
+ )?;
217
+ class.define_method("sanitize", method!(NativeConverter::sanitize, 1))?;
218
+ class.define_method("add_font", method!(NativeConverter::add_font, 2))?;
219
+ class.define_method("add_font_pack", method!(NativeConverter::add_font_pack, 2))?;
220
+ class.define_method("base_path", method!(NativeConverter::base_path, 1))?;
221
+ class.define_method("resource_root", method!(NativeConverter::resource_root, 1))?;
222
+ class.define_method("header", method!(NativeConverter::header, 1))?;
223
+ class.define_method("footer", method!(NativeConverter::footer, 1))?;
224
+ class.define_method("convert", method!(NativeConverter::convert, 1))?;
225
+ class.define_method(
226
+ "convert_markdown",
227
+ method!(NativeConverter::convert_markdown, 1),
228
+ )?;
229
+ class.define_method(
230
+ "convert_to_file",
231
+ method!(NativeConverter::convert_to_file, 2),
232
+ )?;
233
+ class.define_method(
234
+ "convert_markdown_to_file",
235
+ method!(NativeConverter::convert_markdown_to_file, 2),
236
+ )?;
237
+ Ok(())
238
+ }
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ironpress
4
+ VERSION = "1.5.3"
5
+ end
data/lib/ironpress.rb CHANGED
@@ -1,7 +1,73 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "ironpress/ironpress"
3
+ require "rbconfig"
4
+ require_relative "ironpress/version"
5
+
6
+ ruby_api_version = RUBY_VERSION.split(".").first(2).join(".")
7
+ versioned_extension = File.join(__dir__, "ironpress", ruby_api_version, "ironpress_ruby")
8
+ source_extension = File.join(__dir__, "ironpress", "ironpress_ruby")
9
+ extension_suffix = RbConfig::CONFIG.fetch("DLEXT")
10
+ extension = if File.file?("#{versioned_extension}.#{extension_suffix}")
11
+ versioned_extension
12
+ else
13
+ source_extension
14
+ end
15
+ require extension
4
16
 
5
17
  module Ironpress
6
- VERSION = "1.4.1"
18
+ # Mutable Ruby facade over immutable native converter values.
19
+ class HtmlConverter
20
+ CONFIGURATION_METHODS = %i[
21
+ page_size
22
+ page_size_custom
23
+ margin
24
+ margin_sides
25
+ compress
26
+ jpeg_quality
27
+ auto_resize_images
28
+ image_dpi
29
+ filter_dpi
30
+ mask_dpi
31
+ background_raster_dpi
32
+ occlusion_cull
33
+ sanitize
34
+ add_font
35
+ add_font_pack
36
+ base_path
37
+ resource_root
38
+ header
39
+ footer
40
+ ].freeze
41
+
42
+ def initialize
43
+ @native = NativeConverter.new
44
+ end
45
+
46
+ CONFIGURATION_METHODS.each do |name|
47
+ define_method(name) do |*arguments|
48
+ @native = @native.public_send(name, *arguments)
49
+ self
50
+ end
51
+ end
52
+
53
+ def convert(html)
54
+ @native.convert(html)
55
+ end
56
+
57
+ def convert_markdown(markdown)
58
+ @native.convert_markdown(markdown)
59
+ end
60
+
61
+ def convert_to_file(html, path)
62
+ @native.convert_to_file(html, path)
63
+ self
64
+ end
65
+
66
+ def convert_markdown_to_file(markdown, path)
67
+ @native.convert_markdown_to_file(markdown, path)
68
+ self
69
+ end
70
+ end
71
+
72
+ private_constant :NativeConverter
7
73
  end
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.5.2
4
+ version: 1.5.3
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-08-17 00:00:00.000000000 Z
11
+ date: 2026-08-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rb_sys
@@ -24,19 +24,21 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0.9'
27
- description: Convert HTML, CSS, and Markdown to PDF with no browser or system dependencies.
28
- Native Rust extension for maximum performance.
27
+ description: |-
28
+ Convert HTML, CSS, and Markdown to PDF with no browser or system
29
+ dependencies. Native Rust extension for maximum performance.
29
30
  email:
30
31
  executables: []
31
32
  extensions:
32
33
  - ext/ironpress/extconf.rb
33
34
  extra_rdoc_files: []
34
35
  files:
35
- - Cargo.toml
36
36
  - README.md
37
+ - ext/ironpress/Cargo.toml
37
38
  - ext/ironpress/extconf.rb
39
+ - ext/ironpress/src/lib.rs
38
40
  - lib/ironpress.rb
39
- - src/lib.rs
41
+ - lib/ironpress/version.rb
40
42
  homepage: https://github.com/gastongouron/ironpress
41
43
  licenses:
42
44
  - MIT
data/Cargo.toml DELETED
@@ -1,13 +0,0 @@
1
- [package]
2
- name = "ironpress-ruby"
3
- version = "1.5.2"
4
- edition = "2024"
5
- publish = false
6
-
7
- [lib]
8
- name = "ironpress_ruby"
9
- crate-type = ["cdylib"]
10
-
11
- [dependencies]
12
- ironpress-core = { path = "../..", package = "ironpress" }
13
- magnus = "0.7"
data/src/lib.rs DELETED
@@ -1,387 +0,0 @@
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
- };
10
-
11
- fn html_to_pdf(ruby: &Ruby, html: String) -> Result<RString, Error> {
12
- let pdf = ironpress_core::html_to_pdf(&html)
13
- .map_err(|e| Error::new(ruby.exception_runtime_error(), e.to_string()))?;
14
- Ok(ruby.str_from_slice(&pdf))
15
- }
16
-
17
- fn markdown_to_pdf(ruby: &Ruby, markdown: String) -> Result<RString, Error> {
18
- let pdf = ironpress_core::markdown_to_pdf(&markdown)
19
- .map_err(|e| Error::new(ruby.exception_runtime_error(), e.to_string()))?;
20
- Ok(ruby.str_from_slice(&pdf))
21
- }
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
-
338
- #[magnus::init]
339
- fn init(_ruby: &Ruby) -> Result<(), Error> {
340
- let module = define_module("Ironpress")?;
341
-
342
- // Module-level convenience functions (existing API).
343
- module.define_singleton_method("html_to_pdf", function!(html_to_pdf, 1))?;
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
-
386
- Ok(())
387
- }