libpng is a Ruby binding for the official PNG reference library
(libpng). The native shared library
is pre-compiled for each target platform and shipped inside the gem, so
gem install libpng Just Works without a C compiler on the host.
Installation
Add to your Gemfile:
gem "libpng"
Then:
bundle install
Or install manually:
gem install libpng
RubyGems automatically selects the pre-compiled gem that matches your
platform. No C compiler, libpng development headers, or system packages
are required at install time when a binary gem is available. The
platform-agnostic ruby gem is the fallback: it downloads the libpng
source tarball at install time and compiles it via CMake (requires
cmake, ninja, and zlib headers).
Supported platforms
The following 11 pre-compiled platform gems are published for each
release — 10 native targets plus the platform-agnostic source (ruby)
gem (see the RubyGems versions page
for the current set):
| Gem platform | Target | Build host |
|---|---|---|
|
Any platform (compiles on install) |
n/a |
|
x86_64 Linux (glibc) |
|
|
x86_64 Linux (musl, e.g. Alpine) |
|
|
ARM64 Linux (glibc) |
|
|
ARM64 Linux (musl) |
|
|
ARM64 OpenHarmony / Huawei HarmonyOS PC |
|
|
x64 Windows, RubyInstaller < 3.0 (MSVCRT) |
|
|
x64 Windows, RubyInstaller >= 3.0 (UCRT) |
|
|
ARM64 Windows (Ruby >= 3.4) |
|
|
Intel macOS |
|
|
Apple Silicon macOS |
|
Usage
require "libpng"
# Decode a PNG into raw pixels.
decoded = Libpng.decode(File.binread("out.png"), pixel_format: "RGBA")
decoded.width # => Integer
decoded.height # => Integer
decoded.format # => "RGBA"
decoded.pixels # => binary String of RGBA bytes
decoded.bit_depth # => 8 (from IHDR)
decoded.color_type # => 6 (PNG_COLOR_TYPE_RGB_ALPHA, from IHDR)
decoded.interlace # => 0 (PNG_INTERLACE_NONE, from IHDR)
decoded.text # => {"Title" => "Sunset", "Author" => "Jane"} (tEXt/zTXt/iTXt)
decoded.color # => {:gamma => 0.45455, :srgb_intent => 0, ...} (gAMA/cHRM/sRGB/iCCP)
decoded.phys # => {:pixels_per_unit_x => 3779, :unit => 1, :dpi_x => 95.99, ...} (pHYs)
Supported pixel formats
-
"GRAY"/"GRAYSCALE"— 8-bit grayscale (1 byte/pixel) -
"GA"/"AG"— 8-bit grayscale + alpha (2 bytes/pixel) -
"RGB","BGR"— 8-bit truecolor (3 bytes/pixel) -
"RGBA","ARGB","BGRA","ABGR"— 8-bit truecolor + alpha (4 bytes/pixel)
Encoding options
convert_to_8bit: true-
causes libpng to convert 16-bit input to 8-bit on write (input is still passed as a packed byte buffer; use this if you have packed 16-bit-per-channel data).
strip_colorspace: true-
(default) walks the PNG chunk list after encode and keeps only
IHDR,IDAT, andIEND, dropping the sRGB/gAMA chunks libpng’s simplified API emits by default. This matches the "classic" libpng write path (e.g. libemf2svg) and produces byte-identical output. Set tofalseto keep the ancillary color chunks.
Standard API (encode_standard)
For parity with libpng’s classic write path — the same code path used
by libemf2svg’s `rgb2png — use encode_standard instead of
encode. It calls png_create_write_struct + png_set_IHDR
png_set_rows + png_write_png(PNG_TRANSFORM_IDENTITY) directly.
Libpng.encode_standard(width, height, pixels,
pixel_format: "RGBA",
filter: :default,
compression_level: 6,
interlace: :none,
bit_depth: 8,
palette: nil,
# Metadata writers (each optional):
text: { "Title" => "Sunset", "Author" => "Jane" },
gamma: 0.45455,
srgb_intent: 0,
chromaticities: { white_point_x: 0.3127, ... },
icc_profile: { name: "sRGB IEC61966-2.1", data: bytes },
phys: { pixels_per_unit_x: 3779, pixels_per_unit_y: 3779, unit: 1 })
pixel_format-
:gray,:ga,:rgb,:rgba, or:palette. The byte-order variants (BGR,ARGB,BGRA,ABGR) are simplified-API only — the standard API always emits host-order. filter-
:default(adaptive — libpng picks the best filter per row, the libemf2svg default),:adaptive(same output, exercises a different libpng code path),:none,:sub,:up,:avg,:paeth,:all. compression_level-
zlib level 0-9. Default 6 (Z_DEFAULT_COMPRESSION, matching libemf2svg).
interlace-
:none(default) or:adam7. bit_depth-
8 (default) or 16. Only valid for
:gray,:rgb,:rgba. 16-bit callers must provide 2 bytes per channel (host-order little-endian). Always 8 for:palette. palette-
Array of
[r, g, b]or[r, g, b, a](1..256 entries). Required whenpixel_format: :palette. If any entry has alpha, atRNSchunk is emitted.
Output is written directly into a Ruby String via png_set_write_fn — no Tempfile, no disk I/O. The FFI write callback is held on a local
Array so it survives the libpng calls.
Differences from encode:
- Emits only IHDR / IDAT / IEND (no post-hoc chunk stripping
needed).
- Supports palette, 16-bit, and interlaced output — simplified API
doesn’t.
- Errors are raised via a png_set_error_fn callback (set on
png_create_write_struct). libpng expects error callbacks to
longjmp; we rb_raise instead, which unwinds the Ruby stack. The
ensure block calls png_destroy_write_struct to release the C
state.
Standard API (decode_standard)
Libpng.decode_standard is the read-side counterpart to
encode_standard. It calls png_create_read_struct
png_set_read_fn (memory reader) + png_read_info + transforms
png_read_image, giving the caller explicit control over which
transforms apply (palette-to-RGB, gray-to-RGB, alpha add/strip,
16-to-8 demotion, interlace handling).
decoded = Libpng.decode_standard(png_bytes, pixel_format: "RGBA", bit_depth: 8)
Returns the same Libpng::DecodedImage shape as decode, so metadata
fields (text, color, phys) work uniformly across both decode
paths. Use decode for the common case (auto-transforms); use
decode_standard when you need to know exactly which transforms
applied (e.g. for parity with libemf2svg’s `png2rgb).
Ractor safety
Libpng.encode, Libpng.encode_standard, Libpng.decode, and
Libpng.decode_standard are all Ractor-safe. Every call allocates and
frees its own libpng state; no shared mutable state exists on the Ruby
side. Calls from multiple Ractors run concurrently without locking.
Example (Ruby 3.x uses Ractor#take; Ruby 4.0+ uses Ractor#value):
Versioning
This gem follows a {LIBPNG_VERSION}.{ITERATION} scheme:
-
LIBPNG_VERSIONis the upstream libpng release the gem is built against (currently1.6.58). -
ITERATIONis a counter that bumps on Ruby-side changes (recipe bug fixes, CI tweaks, doc updates). It resets to 0 each timeLIBPNG_VERSIONbumps.
For example, 1.6.58.0 is the first release against libpng 1.6.58,
1.6.58.1 is a Ruby-side fix, and 1.6.59.0 would be a release against
the next upstream libpng.
How it works
The gem has three layers:
-
lib/libpng.rb— Ruby FFI wrapper. Loadslibpng16.{so,dylib,dll}from the gem’slib/libpng/directory at runtime and exposesLibpng.encode/Libpng.decodeagainst libpng’s "simplified" high-level C API (png_image_*). The wrapper manually lays out thepng_imagestruct via explicit byte offsets into anFFI::MemoryPointerrather thanFFI::Struct, becauseFFI::Structcarries class-level state that is not shareable across non-main Ractors. -
lib/libpng/recipe.rb— aMiniPortileCMakerecipe that downloads the libpng source tarball pinned byLIBPNG_URL/LIBPNG_SHA256, runs CMake withPNG_SHARED=ON PNG_STATIC=OFF PNG_TESTS=OFF, and copies the resulting shared lib intolib/libpng/. Invoked only when installing the source (rubyplatform) gem. -
ext/extconf.rb— gem extension entry point. CallsRecipe#cook_if_not, which is a no-op once the per-platform checkpoint file (ports/…/<name>-<ver>-<platform>.installed) exists. After cooking, emits a dummyMakefileviamkmfso RubyGems is satisfied (no Ruby C extension is compiled). The pre-compiled platform gems unsetspec.extensions, so this never runs for them.
Pre-compiled platform gems
Rakefile defines gem:native:<platform> tasks for each target. Each
task clones the gemspec, sets spec.platform, adds
lib/libpng/*.{dll,so,dylib} to spec.files, removes the
mini_portile2 dependency (no longer needed at install time), and
unsets spec.extensions — the resulting gem installs with no compiler.
The release workflow (.github/workflows/release.yml) builds all 10
platforms in parallel on the appropriate native runner:
-
x86_64 Linux glibc —
ubuntu-latest -
ARM64 Linux glibc —
ubuntu-24.04-arm(native) -
x86_64 Linux musl —
ruby:<ver>-alpineviadocker runonubuntu-latest -
ARM64 Linux musl —
ruby:<ver>-alpineviadocker runonubuntu-24.04-arm -
x64 Windows (both MSVCRT and UCRT) —
windows-latestwithstep-security/msvc-dev-cmd -
ARM64 Windows —
windows-11-arm(native) witharch: arm64 -
Intel macOS —
macos-15-intel -
Apple Silicon macOS —
macos-latest
Linux glibc and macOS builds run natively on the matching runner. Linux
musl builds run inside ruby:<ver>-alpine containers via docker run
rather than the workflow container: field, because Actions' JS runtime
has no arm64-musl build and therefore cannot run Alpine containers on
arm64 runners. Windows builds use the Visual Studio toolchain via
step-security/msvc-dev-cmd (a maintained drop-in for the deleted
ilammy/msvc-dev-tools).
License
BSD-2-Clause; see LICENSE.txt. The gem bundles pre-compiled libpng binaries; libpng itself is distributed under the libpng license.