fulgur 0.0.1 → 0.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2790fa4c3713ccd81cc9b8892acecf0e805faeea46b331495919e1c703d0a00f
4
- data.tar.gz: 644d46841ef4a505247c8801b1ee3e164a5bcc17f4adfda997c3e809fb78d638
3
+ metadata.gz: e921af1392812aec76a28ea1bbf77598131c39c3fd344159375dd3d9642a2185
4
+ data.tar.gz: 75810a1529a8d327b17950cf60372bf0d0a54f5c1dbf1406297d560e41ad018c
5
5
  SHA512:
6
- metadata.gz: 94ad56b86a3d79499cf60fa6550f37b14f53476422b24a7bf4c5f7854c81fd9015e7defc1f9ba9fee5a993ecdbec0abfbe2a7d78ce9a455e2ef03fd5532836f5
7
- data.tar.gz: ac3682a7518b0d9798526a1b9314bf1034dd316ad8f69c2f7935aff5e5617694333abaa3cfc5184a6c40a558ad535bd3b301b01ecbbf1045d530db52386df206
6
+ metadata.gz: 857299514d3931f5404e1701ae5524984ebbcc4b51e3afc164c558828d6254b6f1e3c59ab34fcf68530d0a63fc47042e85d1408a1f31a178bafd4d2a8c13ecf6
7
+ data.tar.gz: 6d946aad252616dc511afbd17ad895540c5923418be7435ac1648f3aba1a559920a78704a837345f515af97f62bfc254bb4da42b09f52667ba370efa728d2afa
data/CHANGELOG.md ADDED
@@ -0,0 +1,27 @@
1
+ # Changelog
2
+
3
+ All notable changes to the `fulgur` gem will be documented here.
4
+
5
+ ## [Unreleased]
6
+
7
+ ## [0.0.1] - 2026-04-17
8
+
9
+ Initial Ruby binding for fulgur.
10
+
11
+ ### Added
12
+
13
+ - `Fulgur::Engine` (kwargs constructor + builder chain)
14
+ - `Fulgur::EngineBuilder` for reusable engine construction
15
+ - `Fulgur::AssetBundle` with long (`add_*`) and short (`css`, `font_file`, etc.) aliases
16
+ - `Fulgur::PageSize` with `A4` / `LETTER` / `A3` constants and `.custom(w_mm, h_mm)`; accepts `Symbol`, `String`, or class constants as input
17
+ - `Fulgur::Margin` with CSS-style positional args, keyword args, and `.uniform` / `.symmetric` factories
18
+ - `Fulgur::Pdf` result object: `#to_s` (ASCII-8BIT), `#to_base64`, `#to_data_uri`, `#write_to_path`, `#write_to_io` (64 KiB chunked, binmode-guaranteed), `#bytesize`
19
+ - `Engine#render_html` and `Engine#render_html_to_file` release the GVL during the Rust render call
20
+ - Error hierarchy: `Fulgur::Error` / `Fulgur::RenderError` / `Fulgur::AssetError`, plus standard `ArgumentError` / `Errno::ENOENT`
21
+ - Ruby 3.3+ support
22
+
23
+ ### Known Limitations
24
+
25
+ - Precompiled gems / RubyGems publish automation are tracked separately (fulgur-qyf) and not yet in place; gems must be built from source for now
26
+ - Streaming renderer: Krilla emits bytes at the end of rendering, so `#write_to_io` chunks a completed buffer rather than streaming during layout
27
+ - No Ractor safety analysis yet
data/Cargo.toml ADDED
@@ -0,0 +1,30 @@
1
+ [package]
2
+ name = "fulgur-ruby"
3
+ version = "0.5.0"
4
+ # rb_sys cross-gem-action は `directory: crates/fulgur-ruby` のみをコンテナに
5
+ # マウントするため、この Cargo.toml は workspace root 不在でも cargo metadata
6
+ # を解決できる必要がある。したがって workspace 継承 (`.workspace = true`) は
7
+ # 使わず、値は root Cargo.toml [workspace.package] と同期させて inline する。
8
+ edition = "2024"
9
+ rust-version = "1.85.0"
10
+ license = "MIT OR Apache-2.0"
11
+ repository = "https://github.com/mitsuru/fulgur"
12
+ homepage = "https://github.com/mitsuru/fulgur"
13
+ description = "Ruby bindings for fulgur — offline HTML/CSS to PDF conversion"
14
+ publish = false
15
+
16
+ [lib]
17
+ name = "fulgur"
18
+ crate-type = ["cdylib", "rlib"]
19
+
20
+ [features]
21
+ ruby-api = ["dep:magnus", "dep:rb-sys"]
22
+
23
+ [dependencies]
24
+ fulgur = { path = "../fulgur" }
25
+ base64 = "0.22"
26
+ magnus = { version = "0.7", optional = true }
27
+ rb-sys = { version = "0.9", optional = true }
28
+ # Send/Sync 保証 (Engine) のコンパイル時 assertion で使用。
29
+ # ext/fulgur/Cargo.toml にも同じ依存を追加していること。
30
+ static_assertions = "1.1"
data/README.md CHANGED
@@ -1,28 +1,228 @@
1
1
  # fulgur
2
2
 
3
- Ruby bindings for [fulgur](https://github.com/mitsuru/fulgur) — an offline, deterministic HTML/CSS to PDF conversion library written in Rust.
3
+ Ruby bindings for [fulgur](https://github.com/mitsuru/fulgur) — an offline,
4
+ deterministic HTML/CSS to PDF conversion library written in Rust.
4
5
 
5
6
  ## Status
6
7
 
7
- **This gem is a name reservation.** The implementation is under active development.
8
+ **MVP (gem v0.0.1, unreleased).** The core `Engine` / `EngineBuilder` /
9
+ `AssetBundle` / `PageSize` / `Margin` / `Pdf` API is available. Precompiled
10
+ gems, batch rendering, sandboxing, and template-engine wiring are planned
11
+ for later releases.
8
12
 
9
- ## Planned API
13
+ > **Versioning note:** the `fulgur` gem is versioned independently from the
14
+ > underlying `fulgur` Rust crate and the `pyfulgur` PyPI package. This gem
15
+ > starts at `0.0.1` as an MVP and will bump on its own cadence. "v0.5.0"
16
+ > elsewhere in project documents refers to the parent epic that bundles
17
+ > the Rust / Python / Ruby shipping milestones, not this gem's version.
10
18
 
11
- > **Not available in v0.0.1.** The current release is a placeholder for name reservation.
19
+ ## Install
20
+
21
+ > **Note:** v0.0.1 is an early MVP. Pre-built gems are not yet published to
22
+ > RubyGems; build from source for now.
23
+
24
+ Requires a Rust toolchain (1.85+) and Ruby 3.3+.
25
+
26
+ ```bash
27
+ # From a checkout of the fulgur repository
28
+ cd crates/fulgur-ruby
29
+ bundle install
30
+ bundle exec rake compile
31
+ ```
32
+
33
+ Once pre-built gems ship, installation will be:
34
+
35
+ ```bash
36
+ gem install fulgur
37
+ ```
38
+
39
+ For Bundler:
40
+
41
+ ```ruby
42
+ gem "fulgur"
43
+ ```
44
+
45
+ ## Quick start
12
46
 
13
47
  ```ruby
14
48
  require "fulgur"
15
49
 
16
50
  bundle = Fulgur::AssetBundle.new
17
51
  bundle.add_css("body { font-family: sans-serif; }")
18
- bundle.add_font_file("fonts/NotoSans-Regular.ttf")
19
52
 
20
- engine = Fulgur::Engine.builder.page_size("A4").assets(bundle).build
21
- pdf_bytes = engine.render_html("<h1>Hello, world!</h1>")
53
+ engine = Fulgur::Engine.new(page_size: :a4, assets: bundle)
54
+ pdf = engine.render_html("<h1>Hello, world!</h1>")
55
+
56
+ pdf.write_to_path("output.pdf")
57
+ ```
58
+
59
+ Builder style:
60
+
61
+ ```ruby
62
+ engine = Fulgur::Engine.builder
63
+ .page_size(Fulgur::PageSize::A4)
64
+ .landscape(false)
65
+ .title("My doc")
66
+ .assets(bundle)
67
+ .build
68
+
69
+ engine.render_html_to_file("<h1>Hi</h1>", "out.pdf")
70
+ ```
71
+
72
+ ## API surface
73
+
74
+ ### `Fulgur::Engine`
75
+
76
+ Keyword-argument constructor:
77
+
78
+ ```ruby
79
+ engine = Fulgur::Engine.new(
80
+ page_size: :a4, # Symbol, String, or Fulgur::PageSize
81
+ margin: Fulgur::Margin.uniform(72),
82
+ landscape: false,
83
+ title: "My Document",
84
+ author: "Me",
85
+ lang: "en",
86
+ bookmarks: true,
87
+ assets: bundle, # Fulgur::AssetBundle
88
+ )
89
+ ```
90
+
91
+ Or use the builder:
92
+
93
+ ```ruby
94
+ engine = Fulgur::Engine.builder
95
+ .page_size(:letter)
96
+ .margin(Fulgur::Margin.new(72, 36, 48, 24))
97
+ .assets(bundle)
98
+ .build
99
+ ```
100
+
101
+ ### Rendering
22
102
 
23
- File.binwrite("output.pdf", pdf_bytes)
103
+ ```ruby
104
+ pdf = engine.render_html(html_string) # => Fulgur::Pdf
105
+ engine.render_html_to_file(html, "out.pdf") # shortcut
106
+ ```
107
+
108
+ `render_html` and `render_html_to_file` release the GVL, allowing other
109
+ Ruby threads to run concurrently during rendering.
110
+
111
+ ### `Fulgur::Pdf` (render result)
112
+
113
+ ```ruby
114
+ pdf.bytesize # => Integer
115
+ pdf.to_s # => String (ASCII-8BIT binary)
116
+ pdf.to_base64 # => String (Base64)
117
+ pdf.to_data_uri # => "data:application/pdf;base64,..."
118
+ pdf.write_to_path("out.pdf") # write raw bytes to file path
119
+ pdf.write_to_io(io) # chunked write to any IO (calls binmode when supported)
24
120
  ```
25
121
 
122
+ The result object keeps bytes on the Rust side. Methods like `to_base64`
123
+ encode directly from the Rust buffer, avoiding an intermediate Ruby binary
124
+ String. For server-side batch workloads rendering many PDFs, this halves
125
+ peak memory compared with `Base64.strict_encode64(bytes)` on the Ruby
126
+ side.
127
+
128
+ ### `Fulgur::AssetBundle`
129
+
130
+ Offline-first: all assets must be explicitly registered.
131
+
132
+ ```ruby
133
+ bundle = Fulgur::AssetBundle.new
134
+ bundle.add_css("body { font-family: 'Noto Sans' }")
135
+ bundle.add_css_file("style.css")
136
+ bundle.add_font_file("NotoSans-Regular.ttf")
137
+ bundle.add_image("logo", File.binread("logo.png"))
138
+ bundle.add_image_file("icon", "icon.png")
139
+ ```
140
+
141
+ Short aliases are also available:
142
+
143
+ ```ruby
144
+ bundle.css "..."
145
+ bundle.css_file "style.css"
146
+ bundle.font_file "NotoSans.ttf"
147
+ bundle.image "logo", bytes
148
+ bundle.image_file "icon", "icon.png"
149
+ ```
150
+
151
+ ### `Fulgur::PageSize`
152
+
153
+ ```ruby
154
+ Fulgur::PageSize::A4
155
+ Fulgur::PageSize::LETTER
156
+ Fulgur::PageSize::A3
157
+ Fulgur::PageSize.custom(100, 200) # width/height in mm
158
+ ```
159
+
160
+ Engine kwargs and builder also accept `:a4`, `"A4"`, etc. as shorthand.
161
+
162
+ ### `Fulgur::Margin`
163
+
164
+ ```ruby
165
+ Fulgur::Margin.new(72) # uniform
166
+ Fulgur::Margin.new(72, 36) # [vertical, horizontal]
167
+ Fulgur::Margin.new(72, 36, 48, 24) # [top, right, bottom, left]
168
+ Fulgur::Margin.new(top: 72, right: 36, bottom: 48, left: 24)
169
+ Fulgur::Margin.uniform(72)
170
+ Fulgur::Margin.symmetric(72, 36)
171
+ ```
172
+
173
+ All values are in points (pt).
174
+
175
+ ## LLM integration
176
+
177
+ `Fulgur::Pdf#to_base64` and `#to_data_uri` are optimized for passing PDFs
178
+ to LLMs as base64-encoded payloads (e.g., Anthropic Claude, OpenAI GPT-4):
179
+
180
+ ```ruby
181
+ pdf = engine.render_html(html)
182
+
183
+ anthropic.messages.create(
184
+ model: "claude-opus-4-7",
185
+ messages: [{
186
+ role: "user",
187
+ content: [
188
+ {
189
+ type: "document",
190
+ source: {
191
+ type: "base64",
192
+ media_type: "application/pdf",
193
+ data: pdf.to_base64,
194
+ },
195
+ },
196
+ { type: "text", text: "Summarize this document." },
197
+ ],
198
+ }],
199
+ )
200
+ ```
201
+
202
+ ## Errors
203
+
204
+ ```text
205
+ Fulgur::Error # base class (StandardError)
206
+ Fulgur::RenderError # rendering failure (HTML parse, layout, PDF generation, WOFF decode)
207
+ Fulgur::AssetError # asset registration failure (unsupported font format, invalid asset)
208
+
209
+ ArgumentError # invalid arguments (unknown page_size, malformed margin)
210
+ Errno::ENOENT # missing font/image/CSS file
211
+ ```
212
+
213
+ ## Development
214
+
215
+ ```bash
216
+ cd crates/fulgur-ruby
217
+ bundle install
218
+ bundle exec rake compile # builds the Rust extension
219
+ bundle exec rspec # runs tests
220
+ bundle exec rake # compile + test
221
+ ```
222
+
223
+ The native extension lives under `ext/fulgur/`, the Ruby-side wrappers
224
+ under `lib/`, and Rust sources under `src/`.
225
+
26
226
  ## Links
27
227
 
28
228
  - [fulgur on GitHub](https://github.com/mitsuru/fulgur)
@@ -30,4 +230,5 @@ File.binwrite("output.pdf", pdf_bytes)
30
230
 
31
231
  ## License
32
232
 
33
- Licensed under either of Apache License, Version 2.0 or MIT license at your option.
233
+ Licensed under either of Apache License, Version 2.0 or MIT license at
234
+ your option.
@@ -0,0 +1,30 @@
1
+ [workspace]
2
+
3
+ [package]
4
+ name = "fulgur"
5
+ version = "0.5.0"
6
+ # crates/fulgur-ruby/Cargo.toml と同じ src を共有 (path = "../../src/lib.rs")。
7
+ # workspace と同じ edition 2024 に揃える。
8
+ edition = "2024"
9
+
10
+ [lib]
11
+ name = "fulgur"
12
+ crate-type = ["cdylib"]
13
+ path = "../../src/lib.rs"
14
+
15
+ [features]
16
+ default = ["ruby-api"]
17
+ ruby-api = ["dep:magnus", "dep:rb-sys"]
18
+
19
+ [dependencies]
20
+ # cross-gem-action は `directory: crates/fulgur-ruby` しか container にマウントしない
21
+ # ため、`path = "../../../fulgur"` では cargo がパスを解決できない。crates.io 公開版
22
+ # を参照する (release 時は release.yml → release: published → release-ruby.yml の順で
23
+ # 発火するため、この時点で fulgur のバージョンは crates.io に存在する)。
24
+ # release-prepare.yml が sed でこの行のバージョンを同期する。
25
+ fulgur = "0.5.0"
26
+ base64 = "0.22"
27
+ magnus = { version = "0.7", optional = true }
28
+ rb-sys = { version = "0.9", optional = true }
29
+ # 親 crate の src/lib.rs `mod assertions` が使用。
30
+ static_assertions = "1.1"
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mkmf"
4
+ require "rb_sys/mkmf"
5
+
6
+ create_rust_makefile("fulgur/fulgur") do |r|
7
+ r.features = ["ruby-api"]
8
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fulgur
4
+ class AssetBundle
5
+ alias_method :css, :add_css
6
+ alias_method :css_file, :add_css_file
7
+ alias_method :font_file, :add_font_file
8
+ alias_method :image, :add_image
9
+ alias_method :image_file, :add_image_file
10
+ end
11
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fulgur
4
+ class Margin
5
+ class << self
6
+ alias_method :__native_new__, :new
7
+
8
+ def new(*args, **kwargs)
9
+ unless kwargs.empty?
10
+ raise ArgumentError, "positional and kwargs are mutually exclusive" unless args.empty?
11
+
12
+ required = %i[top right bottom left]
13
+ missing = required - kwargs.keys
14
+ raise ArgumentError, "missing keys: #{missing.join(", ")}" unless missing.empty?
15
+
16
+ t, r, b, l = kwargs.values_at(*required).map(&:to_f)
17
+ return __build__(t, r, b, l)
18
+ end
19
+
20
+ case args.length
21
+ when 1
22
+ v = args[0].to_f
23
+ __build__(v, v, v, v)
24
+ when 2
25
+ vv, hh = args.map(&:to_f)
26
+ __build__(vv, hh, vv, hh)
27
+ when 4
28
+ t, r, b, l = args.map(&:to_f)
29
+ __build__(t, r, b, l)
30
+ else
31
+ raise ArgumentError, "wrong number of arguments (#{args.length} for 1, 2, 4, or kwargs)"
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fulgur
4
+ VERSION = "0.5.0"
5
+ end
data/lib/fulgur.rb CHANGED
@@ -1,15 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Fulgur — Ruby bindings for fulgur (HTML/CSS to PDF).
4
- #
5
- # This is a placeholder gem. The implementation is under active development.
6
- # See https://github.com/mitsuru/fulgur for details.
7
- module Fulgur
8
- VERSION = "0.0.1"
3
+ require_relative "fulgur/version"
9
4
 
10
- def self.placeholder
11
- raise NotImplementedError,
12
- "fulgur gem is not yet implemented. " \
13
- "See https://github.com/mitsuru/fulgur for progress."
14
- end
5
+ begin
6
+ minor = RUBY_VERSION[/\A\d+\.\d+/]
7
+ require_relative "fulgur/#{minor}/fulgur"
8
+ rescue LoadError
9
+ require_relative "fulgur/fulgur"
15
10
  end
11
+
12
+ module Fulgur
13
+ class Error < StandardError; end
14
+ class RenderError < Error; end
15
+ class AssetError < Error; end
16
+ end
17
+
18
+ require_relative "fulgur/margin"
19
+ require_relative "fulgur/asset_bundle"
@@ -0,0 +1,78 @@
1
+ //! `Fulgur::AssetBundle` Ruby class wrapping `fulgur::AssetBundle`.
2
+ //!
3
+ //! Ruby は GVL 下で single-threaded なので `RefCell` による内部可変で十分。
4
+ //! Engine builder に渡すときは `take_inner()` で中身を奪い、空の
5
+ //! `AssetBundle::new()` に差し替える。
6
+
7
+ use crate::error::map_fulgur_error;
8
+ // `fulgur::AssetBundle` root re-export は 0.4.5 には存在しないため full path で参照。
9
+ use fulgur::asset::AssetBundle;
10
+ use magnus::{Error, Module, RModule, RString, Ruby, function, method, prelude::*};
11
+ use std::cell::RefCell;
12
+ use std::path::PathBuf;
13
+
14
+ #[magnus::wrap(class = "Fulgur::AssetBundle", free_immediately, size)]
15
+ pub struct RbAssetBundle {
16
+ pub(crate) inner: RefCell<AssetBundle>,
17
+ }
18
+
19
+ impl RbAssetBundle {
20
+ pub(crate) fn new() -> Self {
21
+ Self {
22
+ inner: RefCell::new(AssetBundle::new()),
23
+ }
24
+ }
25
+
26
+ /// Engine builder に渡すために中身を取り出す。
27
+ /// 奪った後は empty AssetBundle に差し替える。
28
+ #[allow(dead_code)] // Task 6 で engine builder から呼ばれる
29
+ pub(crate) fn take_inner(&self) -> AssetBundle {
30
+ std::mem::replace(&mut *self.inner.borrow_mut(), AssetBundle::new())
31
+ }
32
+
33
+ fn add_css(&self, css: String) {
34
+ self.inner.borrow_mut().add_css(css);
35
+ }
36
+
37
+ fn add_css_file(&self, path: String) -> Result<(), Error> {
38
+ let ruby = Ruby::get().expect("ruby vm");
39
+ self.inner
40
+ .borrow_mut()
41
+ .add_css_file(PathBuf::from(path))
42
+ .map_err(|e| map_fulgur_error(&ruby, e))
43
+ }
44
+
45
+ fn add_font_file(&self, path: String) -> Result<(), Error> {
46
+ let ruby = Ruby::get().expect("ruby vm");
47
+ self.inner
48
+ .borrow_mut()
49
+ .add_font_file(PathBuf::from(path))
50
+ .map_err(|e| map_fulgur_error(&ruby, e))
51
+ }
52
+
53
+ fn add_image(&self, name: String, data: RString) {
54
+ // SAFETY: `as_slice` returns a reference tied to the Ruby VM lifetime;
55
+ // we immediately copy into an owned `Vec<u8>` so the borrow ends here.
56
+ let bytes = unsafe { data.as_slice() }.to_vec();
57
+ self.inner.borrow_mut().add_image(name, bytes);
58
+ }
59
+
60
+ fn add_image_file(&self, name: String, path: String) -> Result<(), Error> {
61
+ let ruby = Ruby::get().expect("ruby vm");
62
+ self.inner
63
+ .borrow_mut()
64
+ .add_image_file(name, PathBuf::from(path))
65
+ .map_err(|e| map_fulgur_error(&ruby, e))
66
+ }
67
+ }
68
+
69
+ pub fn define(_ruby: &Ruby, fulgur: &RModule) -> Result<(), Error> {
70
+ let class = fulgur.define_class("AssetBundle", magnus::class::object())?;
71
+ class.define_singleton_method("new", function!(RbAssetBundle::new, 0))?;
72
+ class.define_method("add_css", method!(RbAssetBundle::add_css, 1))?;
73
+ class.define_method("add_css_file", method!(RbAssetBundle::add_css_file, 1))?;
74
+ class.define_method("add_font_file", method!(RbAssetBundle::add_font_file, 1))?;
75
+ class.define_method("add_image", method!(RbAssetBundle::add_image, 2))?;
76
+ class.define_method("add_image_file", method!(RbAssetBundle::add_image_file, 2))?;
77
+ Ok(())
78
+ }