forme-ruby 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +35 -0
- data/CODE_OF_CONDUCT.md +18 -0
- data/CONTRIBUTING.md +67 -0
- data/LICENSE +21 -0
- data/NOTICE +5 -0
- data/README.md +71 -0
- data/SECURITY.md +17 -0
- data/THIRD_PARTY_LICENSES.txt +24354 -0
- data/docs/README.md +18 -0
- data/docs/github-setup.md +60 -0
- data/docs/installation.md +60 -0
- data/docs/rails.md +56 -0
- data/docs/releasing.md +83 -0
- data/docs/rendering.md +63 -0
- data/docs/security-exceptions.md +41 -0
- data/examples/hello.rb +6 -0
- data/ext/forme_pdf/Cargo.lock +1156 -0
- data/ext/forme_pdf/Cargo.toml +16 -0
- data/ext/forme_pdf/extconf.rb +31 -0
- data/ext/forme_pdf/src/lib.rs +191 -0
- data/lib/forme_pdf/errors.rb +7 -0
- data/lib/forme_pdf/native.rb +41 -0
- data/lib/forme_pdf/rails.rb +15 -0
- data/lib/forme_pdf/renderer.rb +40 -0
- data/lib/forme_pdf/result.rb +5 -0
- data/lib/forme_pdf/version.rb +6 -0
- data/lib/forme_pdf.rb +25 -0
- metadata +126 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "forme-pdf-native"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
edition = "2021"
|
|
5
|
+
license = "MIT"
|
|
6
|
+
[lib]
|
|
7
|
+
name = "forme_pdf_native"
|
|
8
|
+
crate-type = ["cdylib", "rlib"]
|
|
9
|
+
[dependencies]
|
|
10
|
+
forme-pdf-html = { git = "https://github.com/danmolitor/forme", rev = "f408920e632c59da0651b5b6d32f8c1397477673" }
|
|
11
|
+
serde = { version = "1", features = ["derive"] }
|
|
12
|
+
serde_json = "1"
|
|
13
|
+
base64 = "0.22"
|
|
14
|
+
ttf-parser = "0.25"
|
|
15
|
+
[profile.release]
|
|
16
|
+
panic = "unwind"
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "rbconfig"
|
|
5
|
+
require "open3"
|
|
6
|
+
require "json"
|
|
7
|
+
require "tempfile"
|
|
8
|
+
|
|
9
|
+
keep_build = ARGV.delete("--keep-build")
|
|
10
|
+
Dir.chdir(__dir__) do
|
|
11
|
+
abort "forme-ruby source installation requires Rust/Cargo. Install a supported platform gem or install Rust first." unless system("cargo", "--version", out: File::NULL)
|
|
12
|
+
output, status = Open3.capture2("cargo", "build", "--locked", "--release", "--message-format=json", "--target-dir", File.join(__dir__, "target"), "--manifest-path", File.join(__dir__, "Cargo.toml"))
|
|
13
|
+
abort "Forme native build failed" unless status.success?
|
|
14
|
+
extension = RbConfig::CONFIG["host_os"].include?("darwin") ? "dylib" : "so"
|
|
15
|
+
artifacts = output.lines.filter_map do |line|
|
|
16
|
+
record = JSON.parse(line)
|
|
17
|
+
record.fetch("filenames", []) if record["reason"] == "compiler-artifact" && record.dig("target", "name") == "forme_pdf_native"
|
|
18
|
+
end.flatten
|
|
19
|
+
source = artifacts.find { |path| File.basename(path) == "libforme_pdf_native.#{extension}" }
|
|
20
|
+
abort "Cargo did not produce a Forme shared library for this platform" unless source
|
|
21
|
+
destination = File.expand_path("../../lib/forme_pdf/native", __dir__)
|
|
22
|
+
FileUtils.mkdir_p(destination)
|
|
23
|
+
# Replace the inode atomically: overwriting a loaded signed dylib breaks macOS code-signing caches.
|
|
24
|
+
Tempfile.create(["forme-native-", ".tmp"], destination) do |temporary|
|
|
25
|
+
FileUtils.cp(source, temporary.path)
|
|
26
|
+
temporary.chmod(0o644)
|
|
27
|
+
File.rename(temporary.path, File.join(destination, File.basename(source)))
|
|
28
|
+
end
|
|
29
|
+
FileUtils.rm_rf(File.join(__dir__, "target")) unless keep_build
|
|
30
|
+
File.write("Makefile", "all:\n\t@true\ninstall:\n\t@true\nclean:\n\t@true\n")
|
|
31
|
+
end
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
//! Native result ownership boundary. Each call owns an independent result.
|
|
2
|
+
use base64::{engine::general_purpose::STANDARD, Engine};
|
|
3
|
+
use forme_pdf_html::{render_html, FontSpec, HtmlOptions};
|
|
4
|
+
use serde::Deserialize;
|
|
5
|
+
use std::{
|
|
6
|
+
panic::{catch_unwind, AssertUnwindSafe},
|
|
7
|
+
ptr, slice,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
#[derive(Default, Deserialize)]
|
|
11
|
+
#[serde(default, deny_unknown_fields)]
|
|
12
|
+
struct Options {
|
|
13
|
+
css: Option<String>,
|
|
14
|
+
fonts: Vec<Font>,
|
|
15
|
+
}
|
|
16
|
+
#[derive(Deserialize)]
|
|
17
|
+
#[serde(deny_unknown_fields)]
|
|
18
|
+
struct Font {
|
|
19
|
+
family: String,
|
|
20
|
+
data: String,
|
|
21
|
+
weight: u32,
|
|
22
|
+
italic: bool,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
pub struct RenderResult {
|
|
26
|
+
status: i32,
|
|
27
|
+
pdf: Vec<u8>,
|
|
28
|
+
metadata: Vec<u8>,
|
|
29
|
+
error: Vec<u8>,
|
|
30
|
+
}
|
|
31
|
+
impl RenderResult {
|
|
32
|
+
fn error(status: i32, message: String) -> Self {
|
|
33
|
+
Self {
|
|
34
|
+
status,
|
|
35
|
+
pdf: vec![],
|
|
36
|
+
metadata: vec![],
|
|
37
|
+
error: message.into_bytes(),
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
fn render(html: &[u8], options: &[u8]) -> Result<RenderResult, String> {
|
|
42
|
+
let html = std::str::from_utf8(html).map_err(|_| "HTML must be UTF-8")?;
|
|
43
|
+
let input: Options = serde_json::from_slice(options).map_err(|e| e.to_string())?;
|
|
44
|
+
let mut opts = HtmlOptions {
|
|
45
|
+
css: input.css,
|
|
46
|
+
..HtmlOptions::default()
|
|
47
|
+
};
|
|
48
|
+
for font in input.fonts {
|
|
49
|
+
let data = STANDARD
|
|
50
|
+
.decode(font.data)
|
|
51
|
+
.map_err(|_| "Invalid font Base64")?;
|
|
52
|
+
ttf_parser::Face::parse(&data, 0).map_err(|_| "Invalid font data")?;
|
|
53
|
+
if font.family.is_empty() || !(1..=1000).contains(&font.weight) {
|
|
54
|
+
return Err("Invalid font family or weight".into());
|
|
55
|
+
}
|
|
56
|
+
opts.fonts.push(FontSpec {
|
|
57
|
+
family: font.family,
|
|
58
|
+
data,
|
|
59
|
+
weight: font.weight,
|
|
60
|
+
italic: font.italic,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
let output = render_html(html, &opts).map_err(|e| e.to_string())?;
|
|
64
|
+
let metadata = serde_json::to_vec(
|
|
65
|
+
&serde_json::json!({"warnings": output.warnings, "passes": output.passes}),
|
|
66
|
+
)
|
|
67
|
+
.map_err(|e| e.to_string())?;
|
|
68
|
+
Ok(RenderResult {
|
|
69
|
+
status: 0,
|
|
70
|
+
pdf: output.pdf,
|
|
71
|
+
metadata,
|
|
72
|
+
error: vec![],
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
#[no_mangle]
|
|
76
|
+
pub extern "C" fn forme_abi_version() -> u32 {
|
|
77
|
+
1
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/// Render valid caller-owned buffers; null input is allowed only at length zero.
|
|
81
|
+
/// # Safety
|
|
82
|
+
/// Non-null pointers must address readable buffers of the supplied lengths.
|
|
83
|
+
#[no_mangle]
|
|
84
|
+
pub unsafe extern "C" fn forme_render_html(
|
|
85
|
+
html: *const u8,
|
|
86
|
+
html_len: usize,
|
|
87
|
+
options: *const u8,
|
|
88
|
+
options_len: usize,
|
|
89
|
+
) -> *mut RenderResult {
|
|
90
|
+
let output = catch_unwind(AssertUnwindSafe(|| {
|
|
91
|
+
if (html.is_null() && html_len != 0)
|
|
92
|
+
|| (options.is_null() && options_len != 0)
|
|
93
|
+
|| html_len > isize::MAX as usize
|
|
94
|
+
|| options_len > isize::MAX as usize
|
|
95
|
+
{
|
|
96
|
+
return RenderResult::error(1, "Invalid input buffer".into());
|
|
97
|
+
}
|
|
98
|
+
let h = if html_len == 0 {
|
|
99
|
+
&[]
|
|
100
|
+
} else {
|
|
101
|
+
slice::from_raw_parts(html, html_len)
|
|
102
|
+
};
|
|
103
|
+
let o = if options_len == 0 {
|
|
104
|
+
b"{}"
|
|
105
|
+
} else {
|
|
106
|
+
slice::from_raw_parts(options, options_len)
|
|
107
|
+
};
|
|
108
|
+
render(h, o).unwrap_or_else(|e| RenderResult::error(1, e))
|
|
109
|
+
}))
|
|
110
|
+
.unwrap_or_else(|_| RenderResult::error(2, "Native renderer panicked".into()));
|
|
111
|
+
Box::into_raw(Box::new(output))
|
|
112
|
+
}
|
|
113
|
+
/// # Safety
|
|
114
|
+
/// Result must be a live handle returned by forme_render_html, or null.
|
|
115
|
+
#[no_mangle]
|
|
116
|
+
pub unsafe extern "C" fn forme_result_status(result: *const RenderResult) -> i32 {
|
|
117
|
+
result.as_ref().map_or(3, |r| r.status)
|
|
118
|
+
}
|
|
119
|
+
/// Borrow result bytes: field 0 = PDF, 1 = metadata JSON, 2 = error UTF-8.
|
|
120
|
+
/// # Safety
|
|
121
|
+
/// Result must be live and len must point to writable size_t storage.
|
|
122
|
+
#[no_mangle]
|
|
123
|
+
pub unsafe extern "C" fn forme_result_bytes(
|
|
124
|
+
result: *const RenderResult,
|
|
125
|
+
field: u32,
|
|
126
|
+
len: *mut usize,
|
|
127
|
+
) -> *const u8 {
|
|
128
|
+
if len.is_null() {
|
|
129
|
+
return ptr::null();
|
|
130
|
+
}
|
|
131
|
+
*len = 0;
|
|
132
|
+
let Some(r) = result.as_ref() else {
|
|
133
|
+
return ptr::null();
|
|
134
|
+
};
|
|
135
|
+
let bytes = match field {
|
|
136
|
+
0 => &r.pdf,
|
|
137
|
+
1 => &r.metadata,
|
|
138
|
+
2 => &r.error,
|
|
139
|
+
_ => return ptr::null(),
|
|
140
|
+
};
|
|
141
|
+
*len = bytes.len();
|
|
142
|
+
bytes.as_ptr()
|
|
143
|
+
}
|
|
144
|
+
/// # Safety
|
|
145
|
+
/// Handle must be null or a live allocation from forme_render_html, destroyed once.
|
|
146
|
+
#[no_mangle]
|
|
147
|
+
pub unsafe extern "C" fn forme_result_destroy(result: *mut RenderResult) {
|
|
148
|
+
if !result.is_null() {
|
|
149
|
+
drop(Box::from_raw(result));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
#[cfg(test)]
|
|
153
|
+
mod tests {
|
|
154
|
+
use super::*;
|
|
155
|
+
#[test]
|
|
156
|
+
fn pdf() {
|
|
157
|
+
assert!(render(b"<p>hello</p>", b"{}")
|
|
158
|
+
.unwrap()
|
|
159
|
+
.pdf
|
|
160
|
+
.starts_with(b"%PDF-"));
|
|
161
|
+
}
|
|
162
|
+
#[test]
|
|
163
|
+
fn invalid_utf8() {
|
|
164
|
+
assert!(render(&[255], b"{}").is_err());
|
|
165
|
+
}
|
|
166
|
+
#[test]
|
|
167
|
+
fn unknown_option() {
|
|
168
|
+
assert!(render(b"hi", br#"{"typo":1}"#).is_err());
|
|
169
|
+
}
|
|
170
|
+
#[test]
|
|
171
|
+
fn null_buffer() {
|
|
172
|
+
unsafe {
|
|
173
|
+
let r = forme_render_html(ptr::null(), 1, ptr::null(), 0);
|
|
174
|
+
assert_ne!(forme_result_status(r), 0);
|
|
175
|
+
forme_result_destroy(r);
|
|
176
|
+
forme_result_destroy(ptr::null_mut());
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
#[test]
|
|
180
|
+
fn parallel() {
|
|
181
|
+
let tasks: Vec<_> = (0..8)
|
|
182
|
+
.map(|i| {
|
|
183
|
+
std::thread::spawn(move || {
|
|
184
|
+
render(format!("<p>{i}</p>").as_bytes(), b"{}").unwrap().pdf
|
|
185
|
+
})
|
|
186
|
+
})
|
|
187
|
+
.collect();
|
|
188
|
+
let pdfs: Vec<_> = tasks.into_iter().map(|t| t.join().unwrap()).collect();
|
|
189
|
+
assert_ne!(pdfs[0], pdfs[1]);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ffi"
|
|
4
|
+
module FormePDF
|
|
5
|
+
# Loads only a library supplied by this gem's build or package.
|
|
6
|
+
module Native
|
|
7
|
+
extend FFI::Library
|
|
8
|
+
|
|
9
|
+
@load_mutex = Mutex.new
|
|
10
|
+
|
|
11
|
+
# Load once and validate the versioned C ABI before use.
|
|
12
|
+
def self.load!
|
|
13
|
+
@load_mutex.synchronize do
|
|
14
|
+
return if @loaded
|
|
15
|
+
extension = FFI::Platform.mac? ? "dylib" : "so"
|
|
16
|
+
path = File.expand_path("native/libforme_pdf_native.#{extension}", __dir__)
|
|
17
|
+
fail FormePDF::LoadError, "Forme native library missing; reinstall forme-ruby for your platform (source installs require Rust/Cargo)" unless File.file?(path)
|
|
18
|
+
ffi_lib path
|
|
19
|
+
attach_function :forme_abi_version, [], :uint32
|
|
20
|
+
fail FormePDF::LoadError, "Incompatible Forme native ABI" unless forme_abi_version == 1
|
|
21
|
+
attach_function :forme_render_html, [:pointer, :size_t, :pointer, :size_t], :pointer, blocking: true
|
|
22
|
+
attach_function :forme_result_status, [:pointer], :int32
|
|
23
|
+
attach_function :forme_result_bytes, [:pointer, :uint32, :pointer], :pointer
|
|
24
|
+
attach_function :forme_result_destroy, [:pointer], :void
|
|
25
|
+
@loaded = true
|
|
26
|
+
end
|
|
27
|
+
rescue ::LoadError => e
|
|
28
|
+
fail FormePDF::LoadError, "Cannot load Forme native library: #{e.message}"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Copy a borrowed native buffer before its owning result is destroyed.
|
|
32
|
+
def self.copy(result, field)
|
|
33
|
+
length = FFI::MemoryPointer.new(:size_t)
|
|
34
|
+
pointer = forme_result_bytes(result, field, length)
|
|
35
|
+
size = length.read(:size_t)
|
|
36
|
+
return "".b if size.zero?
|
|
37
|
+
fail FormePDF::RenderError, "Native result has a null buffer" if pointer.null?
|
|
38
|
+
pointer.read_string_length(size).b
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "forme_pdf"
|
|
4
|
+
|
|
5
|
+
module FormePDF
|
|
6
|
+
# Explicitly include this module in an ActionController; no Railtie or middleware.
|
|
7
|
+
module Rails
|
|
8
|
+
# Render a caller-selected Rails template into binary PDF bytes.
|
|
9
|
+
# The controller retains responsibility for authorization and send_data.
|
|
10
|
+
def render_forme_pdf(template:, layout:, locals: {}, **options)
|
|
11
|
+
html = render_to_string(template: template, layout: layout, locals: locals, formats: [:html])
|
|
12
|
+
FormePDF.render_html(html, **options)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "base64"
|
|
5
|
+
module FormePDF
|
|
6
|
+
# One native render call with caller-owned input and independently owned output.
|
|
7
|
+
class Renderer
|
|
8
|
+
# Return PDF bytes, renderer warnings and layout pass count.
|
|
9
|
+
def self.call(html, css: nil, fonts: [])
|
|
10
|
+
fail ArgumentError, "HTML must be a String" unless html.is_a?(String)
|
|
11
|
+
html = html.dup
|
|
12
|
+
html.force_encoding(Encoding::UTF_8) if html.encoding == Encoding::BINARY
|
|
13
|
+
fail ArgumentError, "HTML must be valid UTF-8" unless html.valid_encoding?
|
|
14
|
+
html = html.encode(Encoding::UTF_8)
|
|
15
|
+
fail ArgumentError, "CSS must be a String or nil" unless css.nil? || css.is_a?(String)
|
|
16
|
+
fail ArgumentError, "fonts must be an Array" unless fonts.is_a?(Array)
|
|
17
|
+
options = JSON.generate(css: css, fonts: fonts.map { |font|
|
|
18
|
+
fail ArgumentError, "font must contain family and data" unless font.is_a?(Hash) && font[:family].is_a?(String) && font[:data].is_a?(String)
|
|
19
|
+
fail ArgumentError, "unknown font option" unless (font.keys - %i[family data weight italic]).empty?
|
|
20
|
+
{family: font.fetch(:family), data: Base64.strict_encode64(font.fetch(:data)), weight: font.fetch(:weight, 400), italic: font.fetch(:italic, false)}
|
|
21
|
+
})
|
|
22
|
+
Native.load!
|
|
23
|
+
input = FFI::MemoryPointer.from_string(html)
|
|
24
|
+
opts = FFI::MemoryPointer.from_string(options)
|
|
25
|
+
# Async interruption must not arrive between native allocation and ownership.
|
|
26
|
+
Thread.handle_interrupt(Object => :never) do
|
|
27
|
+
result = Native.forme_render_html(input, html.bytesize, opts, options.bytesize)
|
|
28
|
+
fail RenderError, "Native renderer returned no result" if result.null?
|
|
29
|
+
if Native.forme_result_status(result) != 0
|
|
30
|
+
fail RenderError, Native.copy(result, 2).force_encoding(Encoding::UTF_8)
|
|
31
|
+
end
|
|
32
|
+
pdf = Native.copy(result, 0)
|
|
33
|
+
metadata = JSON.parse(Native.copy(result, 1))
|
|
34
|
+
Result.new(pdf: pdf.freeze, warnings: metadata.fetch("warnings").map(&:freeze).freeze, passes: metadata.fetch("passes"))
|
|
35
|
+
ensure
|
|
36
|
+
Native.forme_result_destroy(result) if result && !result.null?
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
data/lib/forme_pdf.rb
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "forme_pdf/version"
|
|
4
|
+
require_relative "forme_pdf/errors"
|
|
5
|
+
require_relative "forme_pdf/result"
|
|
6
|
+
require_relative "forme_pdf/native"
|
|
7
|
+
require_relative "forme_pdf/renderer"
|
|
8
|
+
|
|
9
|
+
module FormePDF
|
|
10
|
+
# Render HTML to a binary PDF String.
|
|
11
|
+
def self.render_html(html, **options)
|
|
12
|
+
render_html_result(html, **options).pdf
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Render HTML and preserve diagnostics.
|
|
16
|
+
def self.render_html_result(html, **options)
|
|
17
|
+
Renderer.call(html, **options)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Validate the gem-owned native library eagerly, e.g. at application startup.
|
|
21
|
+
def self.verify!
|
|
22
|
+
Native.load!
|
|
23
|
+
true
|
|
24
|
+
end
|
|
25
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: forme-ruby
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Ajaya Agrawalla
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-09-19 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: ffi
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '1.17'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '1.17'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: base64
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '0.2'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '0.2'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: json
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '2.0'
|
|
48
|
+
- - "<"
|
|
49
|
+
- !ruby/object:Gem::Version
|
|
50
|
+
version: '3'
|
|
51
|
+
type: :runtime
|
|
52
|
+
prerelease: false
|
|
53
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
54
|
+
requirements:
|
|
55
|
+
- - ">="
|
|
56
|
+
- !ruby/object:Gem::Version
|
|
57
|
+
version: '2.0'
|
|
58
|
+
- - "<"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '3'
|
|
61
|
+
description: Independent Ruby bindings for the Rust Forme HTML renderer. No browser,
|
|
62
|
+
Node.js, WebAssembly or separate rendering service.
|
|
63
|
+
email:
|
|
64
|
+
- ajaya@clearstack.io
|
|
65
|
+
executables: []
|
|
66
|
+
extensions:
|
|
67
|
+
- ext/forme_pdf/extconf.rb
|
|
68
|
+
extra_rdoc_files: []
|
|
69
|
+
files:
|
|
70
|
+
- CHANGELOG.md
|
|
71
|
+
- CODE_OF_CONDUCT.md
|
|
72
|
+
- CONTRIBUTING.md
|
|
73
|
+
- LICENSE
|
|
74
|
+
- NOTICE
|
|
75
|
+
- README.md
|
|
76
|
+
- SECURITY.md
|
|
77
|
+
- THIRD_PARTY_LICENSES.txt
|
|
78
|
+
- docs/README.md
|
|
79
|
+
- docs/github-setup.md
|
|
80
|
+
- docs/installation.md
|
|
81
|
+
- docs/rails.md
|
|
82
|
+
- docs/releasing.md
|
|
83
|
+
- docs/rendering.md
|
|
84
|
+
- docs/security-exceptions.md
|
|
85
|
+
- examples/hello.rb
|
|
86
|
+
- ext/forme_pdf/Cargo.lock
|
|
87
|
+
- ext/forme_pdf/Cargo.toml
|
|
88
|
+
- ext/forme_pdf/extconf.rb
|
|
89
|
+
- ext/forme_pdf/src/lib.rs
|
|
90
|
+
- lib/forme_pdf.rb
|
|
91
|
+
- lib/forme_pdf/errors.rb
|
|
92
|
+
- lib/forme_pdf/native.rb
|
|
93
|
+
- lib/forme_pdf/rails.rb
|
|
94
|
+
- lib/forme_pdf/renderer.rb
|
|
95
|
+
- lib/forme_pdf/result.rb
|
|
96
|
+
- lib/forme_pdf/version.rb
|
|
97
|
+
homepage: https://github.com/clearstackio/forme-ruby
|
|
98
|
+
licenses:
|
|
99
|
+
- MIT
|
|
100
|
+
metadata:
|
|
101
|
+
source_code_uri: https://github.com/clearstackio/forme-ruby
|
|
102
|
+
documentation_uri: https://github.com/clearstackio/forme-ruby/blob/main/docs/README.md
|
|
103
|
+
bug_tracker_uri: https://github.com/clearstackio/forme-ruby/issues
|
|
104
|
+
changelog_uri: https://github.com/clearstackio/forme-ruby/blob/main/CHANGELOG.md
|
|
105
|
+
rubygems_mfa_required: 'true'
|
|
106
|
+
allowed_push_host: https://rubygems.org
|
|
107
|
+
post_install_message:
|
|
108
|
+
rdoc_options: []
|
|
109
|
+
require_paths:
|
|
110
|
+
- lib
|
|
111
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
112
|
+
requirements:
|
|
113
|
+
- - ">="
|
|
114
|
+
- !ruby/object:Gem::Version
|
|
115
|
+
version: '3.2'
|
|
116
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
117
|
+
requirements:
|
|
118
|
+
- - ">="
|
|
119
|
+
- !ruby/object:Gem::Version
|
|
120
|
+
version: '0'
|
|
121
|
+
requirements: []
|
|
122
|
+
rubygems_version: 3.5.22
|
|
123
|
+
signing_key:
|
|
124
|
+
specification_version: 4
|
|
125
|
+
summary: Native HTML to PDF rendering for Ruby and Rails
|
|
126
|
+
test_files: []
|