ironpress 1.5.2 → 1.5.4

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: '06696b437bb27adfde76adbd80dc188f172812023a436b5feca2ade84a84f0a5'
4
+ data.tar.gz: 939d4e115c740e813f039bbfe8d90cc845477d4ad411dcc44ed68a436b654f3d
5
5
  SHA512:
6
- metadata.gz: ccd9fb149b8ac0963fb1d9f560c5f1ec9d846f8a556e18ea18389008edfc99ee0599fd2a4415a64f912d8700a5d6aeb9363b85f748f4afe184e0e6c9a995a8e2
7
- data.tar.gz: 6fccbd311bfdb5812c63432d5acc93cd9a29834ecd0568eb8cbc01f171e13b6ce9912393d514ea045836b57f7005b187bc7b54204f02d6b0e8eb68e58c4735e1
6
+ metadata.gz: 5351e3994374da27eb84f5980a648ec59d70dae33f7f565820b5c0a73838704c1f8c424479c49242ff4920a9b7acb15cfdd3aa037ff8fc2163b530c3aa934a21
7
+ data.tar.gz: 781112a486eefcb602499c757865950921fb581e278d113ba9c3c95f0395708acad813682117f82508d7f5d931f21aa98d51782778ad37d401a57bd6b6b5584b
data/README.md CHANGED
@@ -1,35 +1,86 @@
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 output from HTML or Markdown with a native Rust renderer. No
4
+ browser process or system PDF dependency is required.
4
5
 
5
- ## Installation
6
+ [Read the complete Ruby getting-started guide](https://gastongouron.github.io/ironpress/get-started/ruby/).
6
7
 
7
- ```ruby
8
- gem install ironpress
8
+ ## Requirements
9
+
10
+ - Ruby 3.0 or later
11
+ - Linux, macOS, or Windows
12
+
13
+ Releases include supported native gems plus a source gem.
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ bundle add ironpress
9
19
  ```
10
20
 
11
- Or in your Gemfile:
21
+ Or install the gem globally:
12
22
 
13
- ```ruby
14
- gem "ironpress"
23
+ ```bash
24
+ gem install ironpress
15
25
  ```
16
26
 
17
- ## Usage
27
+ ## Create your first PDF
18
28
 
19
29
  ```ruby
20
30
  require "ironpress"
21
31
 
22
- # HTML to PDF
23
- pdf = Ironpress.html_to_pdf("<h1>Hello World</h1><p>Generated with Ironpress.</p>")
32
+ pdf = Ironpress.html_to_pdf("<h1>Hello from Ruby</h1>")
24
33
  File.binwrite("output.pdf", pdf)
34
+ ```
25
35
 
26
- # Markdown to PDF
27
- pdf = Ironpress.markdown_to_pdf("# Hello\n\nGenerated from **Markdown**.")
28
- File.binwrite("output.pdf", pdf)
36
+ The result is a binary Ruby `String`. Write it with `File.binwrite`, return it
37
+ from a web response, or store it in your application.
38
+
39
+ ## Render Markdown
40
+
41
+ ```ruby
42
+ pdf = Ironpress.markdown_to_pdf("# Release notes\n\nEverything shipped.")
29
43
  ```
30
44
 
31
- ## Performance
45
+ ## Configure and reuse a converter
46
+
47
+ ```ruby
48
+ converter = Ironpress::HtmlConverter.new
49
+ .page_size("Letter")
50
+ .margin_sides(36, 48, 36, 48)
51
+ .header("Quarterly report")
52
+ .footer("Page {page} of {pages}")
53
+
54
+ pdf = converter.convert("<h1>Results</h1>")
55
+ markdown_pdf = converter.convert_markdown("# Results")
56
+ ```
57
+
58
+ Configuration methods return the converter, so policies compose in an idiomatic
59
+ chain. Use `convert_to_file` or `convert_markdown_to_file` for direct output.
60
+
61
+ ## Local resources and fonts
62
+
63
+ Local URLs are denied until a canonical boundary is configured:
64
+
65
+ ```ruby
66
+ converter = Ironpress::HtmlConverter.new
67
+ .base_path("templates")
68
+ .resource_root(".")
69
+ .add_font("Inter", File.binread("assets/Inter.ttf"))
70
+ ```
71
+
72
+ Optional fallback packs enter through `add_font_pack`. Valid names are
73
+ `cjk-jp`, `cjk-kr`, `cjk-sc`, `cjk-tc`, and `emoji`. Rendering never downloads
74
+ a font pack.
75
+
76
+ ## Limits and errors
77
+
78
+ - Invalid named settings raise `ArgumentError`.
79
+ - Conversion failures raise `RuntimeError`.
80
+ - HTML sanitization is enabled by default.
81
+ - The binding has no async or streaming conversion API.
82
+ - Remote HTTP document resources are not available.
83
+ - JavaScript and browser DOM execution are not supported.
32
84
 
33
- - **10-100x faster** than browser-based solutions (Puppeteer, wkhtmltopdf)
34
- - **~5MB** native extension, no runtime dependencies
35
- - Instant startup, no browser process
85
+ See the [binding capability matrix](../README.md) for the contract shared with
86
+ Rust, Python, browser WebAssembly, and Node.js.
@@ -0,0 +1,17 @@
1
+ [package]
2
+ name = "ironpress-ruby"
3
+ version = "1.5.4"
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.4", 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.4"
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.4
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-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rb_sys
@@ -24,23 +24,27 @@ 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
43
- metadata: {}
45
+ metadata:
46
+ documentation_uri: https://gastongouron.github.io/ironpress/get-started/ruby/
47
+ source_code_uri: https://github.com/gastongouron/ironpress
44
48
  post_install_message:
45
49
  rdoc_options: []
46
50
  require_paths:
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
- }