Fast DOCX to PDF and page image conversion for Ruby. Ruby FFI bindings for WordCraft, a pure Rust word processor engine.
- In-process. No LibreOffice, no headless browser, no conversion service.
- Fast. A typical document converts in tens of milliseconds.
- Thread-friendly. Conversions release the GVL, so Puma and Sidekiq threads convert in parallel.
- Self-contained. Fonts are bundled; no system packages needed.
- Prebuilt. Native libraries for macOS and Linux (glibc and musl) ship inside the platform gems.
# Gemfile
gem 'wordcraft'Requires Ruby 3.1+.
require 'wordcraft'
WordCraft.open('contract.docx') do |doc|
File.binwrite('contract.pdf', doc.to_pdf)
File.binwrite('page-1.png', doc.to_png(page: 1))
endWordCraft.open(path, markup: false) { |doc| ... } → the block's value
WordCraft.open_buffer(input, markup: false) { |doc| ... } → the block's value
Without a block, both return a WordCraft::Document.
Opens the document and lays it out once. The PDF, the page count and every page image come from that one layout.
| Argument | Type | |
|---|---|---|
path |
String or Pathname |
The file to open. |
input |
String or IO |
The DOCX bytes, or an IO to read them from (StringIO, File, an upload). The IO is rewound after it's read. |
markup: |
Boolean, optional |
Show tracked changes and comments, as in Word's "All Markup" view. Off by default. |
With a block, the document is closed when the block returns or raises. Without a block, close it yourself: a document left open is freed only when Ruby garbage-collects it, which can be much later.
Raises WordCraft::Error when the document can't be opened. RTF and .doc files open too.
pdf = WordCraft.open('contract.docx', &:to_pdf)
pdf = WordCraft.open_buffer(docx, &:to_pdf)
doc = WordCraft.open_buffer(upload)
begin
pdf = doc.to_pdf
thumbnail = doc.to_jpeg(page: 1, dpi: 72)
ensure
doc.close
end| Method | Returns |
|---|---|
doc.page_count |
Integer |
doc.to_pdf |
PDF bytes |
doc.to_png(page:, dpi: 144) |
PNG bytes of one page |
doc.to_jpeg(page:, dpi: 144, quality: 85) |
JPEG bytes of one page; quality is 1 to 100 |
doc.to_rgba(page:, dpi: 144) |
[pixels, width, height]: 8-bit RGBA, row by row, width * height * 4 bytes |
doc.close |
nil. Frees the document; calling it again does nothing. |
doc.closed? |
Boolean |
page: counts from 1. Page images match the PDF's pages. A US Letter page at 144 dpi is 1224 × 1584 pixels.
A document isn't thread-safe: use each one from a single thread. Separate documents convert in parallel.
to_rgba skips image encoding, for handing pages straight to an image library:
pixels, width, height = doc.to_rgba(page: 1)
image = Vips::Image.new_from_memory(pixels, width, height, 4, :uchar)WordCraft.add_font(bytes) / WordCraft.add_font_file(path) make a font available to every document opened after. Both return the number of faces added.
WordCraft.load_system_fonts loads the installed system fonts up front instead of on the first conversion. Call it at boot.
WordCraft.add_font_file('/app/fonts/Brand-Regular.ttf')
WordCraft.load_system_fontsWordCraft::Error is raised when a document can't be opened or converted, and when a closed document is used. A page that doesn't exist, a dpi that isn't positive or a quality outside 1 to 100 raises ArgumentError.
begin
pdf = WordCraft.open_buffer(upload, &:to_pdf)
rescue WordCraft::Error => e
Rails.logger.warn("DOCX conversion failed: #{e.message}")
endSend a converted upload from a Rails controller:
def create
pdf = WordCraft.open_buffer(params[:file], &:to_pdf)
send_data pdf, type: 'application/pdf', filename: 'document.pdf'
rescue WordCraft::Error => e
render plain: e.message, status: :unprocessable_entity
endConvert an Active Storage attachment:
pdf = WordCraft.open_buffer(document.file.download, &:to_pdf)
document.pdf.attach(io: StringIO.new(pdf), filename: "#{document.file.filename.base}.pdf",
content_type: 'application/pdf')Attach a preview of every page:
WordCraft.open_buffer(document.file.download) do |doc|
(1..doc.page_count).each do |page|
document.previews.attach(io: StringIO.new(doc.to_jpeg(page:, dpi: 72)), filename: "page-#{page}.jpg",
content_type: 'image/jpeg')
end
endWordCraft uses the fonts embedded in the document, then installed system fonts, then its bundled fonts. For Word's proprietary fonts it picks metric-compatible open ones when they are installed. For page breaks closest to Word, install them:
# Alpine
RUN apk add --no-cache font-liberation font-carlito
# Debian / Ubuntu
RUN apt-get install -y --no-install-recommends fonts-liberation2 fonts-crosextra-carlitomusl's malloc slows conversions down, especially in parallel. Preload jemalloc:
RUN apk add --no-cache jemalloc
ENV LD_PRELOAD=/usr/lib/libjemalloc.so.2Using this library to convert untrusted documents (user uploads, files from the internet) is not recommended. The conversion runs native code inside your Ruby process with no sandbox.
If you convert untrusted documents, you must use an isolated, sandboxed process.
MIT. The native library bundles WordCraft (MIT OR Apache-2.0) and the Source Sans 3, Source Serif 4, Inter and JetBrains Mono fonts (OFL-1.1); their licenses and WordCraft's attribution list are in licenses/, and WordCraft's notices in NOTICE.