Module: Libpng

Extended by:
FFI::Library
Defined in:
lib/libpng.rb,
lib/libpng/recipe.rb,
lib/libpng/version.rb

Overview

Libpng is a Ruby binding for libpng (the official PNG reference library) via FFI. The native library is pre-compiled for each target platform and shipped inside the gem, so no C compiler is required at install time.

The API mirrors libpng's "simplified" high-level API:

Libpng.encode(width, height, rgba_bytes, pixel_format: "RGBA")
Libpng.decode(png_bytes, pixel_format: "RGBA")

All encode/decode state is per-call (libpng allocates and frees a png_image internally), so calls from different Ractors do not share state. The module's FFI function table is frozen at load time.

Defined Under Namespace

Classes: DecodedImage, Error, Recipe

Constant Summary collapse

FORMAT_FLAG_ALPHA =

PNG_IMAGE_FORMAT_* bit flags and named formats from png.h.

0x01
FORMAT_FLAG_COLOR =
0x02
FORMAT_FLAG_LINEAR =
0x04
FORMAT_FLAG_COLORMAP =
0x08
FORMAT_FLAG_BGR =
0x10
FORMAT_FLAG_AFIRST =
0x20
FORMAT_FLAG_ASSOCIATED_ALPHA =
0x40
FORMAT_GRAY =
0
FORMAT_GA =
FORMAT_FLAG_ALPHA
FORMAT_AG =
FORMAT_FLAG_ALPHA | FORMAT_FLAG_AFIRST
FORMAT_RGB =
FORMAT_FLAG_COLOR
FORMAT_BGR =
FORMAT_FLAG_COLOR | FORMAT_FLAG_BGR
FORMAT_RGBA =
FORMAT_FLAG_COLOR | FORMAT_FLAG_ALPHA
FORMAT_ARGB =
FORMAT_FLAG_COLOR | FORMAT_FLAG_ALPHA | FORMAT_FLAG_AFIRST
FORMAT_BGRA =
FORMAT_FLAG_COLOR | FORMAT_FLAG_ALPHA | FORMAT_FLAG_BGR
FORMAT_ABGR =
FORMAT_FLAG_COLOR | FORMAT_FLAG_ALPHA | FORMAT_FLAG_AFIRST | FORMAT_FLAG_BGR
FORMAT_BY_NAME =
{
  'GRAY' => FORMAT_GRAY,
  'GRAYSCALE' => FORMAT_GRAY,
  'GA' => FORMAT_GA,
  'AG' => FORMAT_AG,
  'RGB' => FORMAT_RGB,
  'BGR' => FORMAT_BGR,
  'RGBA' => FORMAT_RGBA,
  'ARGB' => FORMAT_ARGB,
  'BGRA' => FORMAT_BGRA,
  'ABGR' => FORMAT_ABGR
}.freeze
PNG_IMAGE_VERSION =

png_image::version value libpng checks for. Defined in png.h as PNG_IMAGE_VERSION == 1.

1
PNG_IMAGE_MESSAGE_BYTES =

Buffer size for libpng's per-image error message (PNG_IMAGE_MESSAGE_BYTES). The png_image struct stores a fixed-size char buffer of this length.

64
PNG_IMAGE_SIZE =

png_image struct field offsets (bytes). png_image is laid out as:

void*       opaque             (pointer-width)
png_uint_32 version            (uint32)
png_uint_32 width              (uint32)
png_uint_32 height             (uint32)
png_uint_32 format             (uint32)
png_uint_32 flags              (uint32)
png_uint_32 colormap_entries   (uint32)
png_uint_32 warning_or_error   (uint32)
char[64]    message

Total = pointer-size + 7*4 + 64. The fixed width makes it safe to allocate via FFI::MemoryPointer directly so the wrapper stays Ractor-safe (FFI::Struct has class-level state that isn't shareable across non-main Ractors).

FFI.type_size(:pointer) + (7 * 4) + PNG_IMAGE_MESSAGE_BYTES
PNG_IMAGE_OFF_OPAQUE =
0
PNG_IMAGE_OFF_VERSION =
FFI.type_size(:pointer)
PNG_IMAGE_OFF_WIDTH =
PNG_IMAGE_OFF_VERSION + 4
PNG_IMAGE_OFF_HEIGHT =
PNG_IMAGE_OFF_WIDTH + 4
PNG_IMAGE_OFF_FORMAT =
PNG_IMAGE_OFF_HEIGHT + 4
PNG_IMAGE_OFF_FLAGS =
PNG_IMAGE_OFF_FORMAT + 4
PNG_IMAGE_OFF_COLORMAP_ENTRIES =
PNG_IMAGE_OFF_FLAGS + 4
PNG_IMAGE_OFF_WARNING_OR_ERROR =
PNG_IMAGE_OFF_COLORMAP_ENTRIES + 4
PNG_IMAGE_OFF_MESSAGE =
PNG_IMAGE_OFF_WARNING_OR_ERROR + 4
LIBPNG_VERSION =

Gem version follows the pattern:

{LIBPNG_VERSION}.{LIBPNG_RUBY_ITERATION}

where LIBPNG_VERSION is the upstream libpng release this gem is built against, and LIBPNG_RUBY_ITERATION is a counter for Ruby-side changes (recipe bug fixes, CI changes, docs) that bump without a new libpng release. The iteration resets to 0 each time LIBPNG_VERSION bumps.

'1.6.58'
LIBPNG_RUBY_ITERATION =
0
VERSION =
"#{LIBPNG_VERSION}.#{LIBPNG_RUBY_ITERATION}"

Class Method Summary collapse

Class Method Details

.decode(png, pixel_format: 'RGBA') ⇒ Object

Decode a PNG file into raw pixels.

png String containing PNG file bytes pixel_format desired output format ("RGB", "RGBA", "GRAY", etc.) (default: "RGBA")

Returns a DecodedImage Struct with #width, #height, #format (String), and #pixels (binary String).

Ractor-safe: same reason as encode.



155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
# File 'lib/libpng.rb', line 155

def decode(png, pixel_format: 'RGBA')
  fmt = FORMAT_BY_NAME[pixel_format.to_s.upcase] ||
        raise(Error, "unknown pixel_format #{pixel_format.inspect}")

  img = FFI::MemoryPointer.new(:uint8, PNG_IMAGE_SIZE)
  img.clear
  img.put_uint32(PNG_IMAGE_OFF_VERSION, PNG_IMAGE_VERSION)

  begin
    FFI::MemoryPointer.new(:uint8, png.bytesize) do |in_buf|
      in_buf.write_bytes(png)
      ok = png_image_begin_read_from_memory(img, in_buf, png.bytesize)
      if ok.zero?
        msg = read_message(img)
        raise Error, "png_image_begin_read_from_memory failed: #{msg}"
      end

      img.put_uint32(PNG_IMAGE_OFF_FORMAT, fmt)
      width = img.get_uint32(PNG_IMAGE_OFF_WIDTH)
      height = img.get_uint32(PNG_IMAGE_OFF_HEIGHT)
      stride = width * bytes_per_pixel_for_format(fmt)
      out_size = stride * height

      FFI::MemoryPointer.new(:uint8, out_size) do |out_buf|
        ok = png_image_finish_read(img, nil, out_buf, stride, nil)
        if ok.zero?
          msg = read_message(img)
          raise Error, "png_image_finish_read failed: #{msg}"
        end
        return DecodedImage.new(width: width,
                                height: height,
                                format: pixel_format.to_s.upcase,
                                pixels: out_buf.read_bytes(out_size))
      end
    end
  ensure
    png_image_free(img)
  end
end

.encode(width, height, pixels, pixel_format: 'RGBA', convert_to_8bit: false, strip_colorspace: true) ⇒ Object

Encode raw pixel data as a PNG.

width, height image dimensions in pixels pixels String of raw pixel bytes (row-major, top-down) pixel_format "RGB", "RGBA", "GRAY", "GA", "BGR", "BGRA" (default: "RGBA") convert_to_8bit when true, libpng converts 16-bit input to 8-bit on write (default: false) strip_colorspace when true, removes any sRGB/gAMA/cHRM/iCCP chunks libpng's simplified API emits by default, leaving only IHDR, IDAT, and IEND. This matches the "classic" libpng write path (e.g. libemf2svg) and produces byte-identical output. Default: true.

Returns a String containing the PNG file bytes. Raises Libpng::Error on failure (bad dimensions, insufficient input bytes, invalid pixel_format, or libpng-internal error).

Ractor-safe: every call allocates and frees its own png_image; no shared mutable state.



137
138
139
140
141
142
143
# File 'lib/libpng.rb', line 137

def encode(width, height, pixels, pixel_format: 'RGBA',
           convert_to_8bit: false, strip_colorspace: true)
  raw = encode_ancillary(width, height, pixels,
                         pixel_format: pixel_format,
                         convert_to_8bit: convert_to_8bit)
  strip_colorspace ? strip_ancillary_chunks(raw) : raw
end