Class: Libpng::StandardDecoder

Inherits:
Object
  • Object
show all
Defined in:
lib/libpng/standard_decoder.rb

Overview

Decodes a PNG byte buffer into raw pixels via libpng's standard read API (png_create_read_struct -> png_set_read_fn -> png_read_info -> optional transforms -> png_read_image). This is the read-side counterpart to StandardEncoder; together they expose libpng's full low-level read/write surface, complementing the simplified API.

Why use this instead of Libpng.decode (the simplified path)?

  • Explicit control over which transforms apply (the simplified API auto-applies a fixed set).
  • Useful when the caller wants to inspect the source pixel format before deciding how to expand it (e.g. skip palette expansion for performance on already-RGBA images).

Options:

pixel_format:     The desired output format. One of:
                :gray, :ga, :rgb, :rgba (default).
                Determines which expansion / stripping
                transforms get applied.
bit_depth:        8 (default) or 16. 16-bit output preserves
                the source's full range when the source is
                also 16-bit; otherwise the source is
                promoted (8 -> 16 by zero-extension) or
                demoted (16 -> 8 by png_set_strip_16).

Output:

Returns a Libpng::DecodedImage. The :text, :color, :phys
metadata fields are populated via ChunkWalker (same as the
simplified path) so callers get the same metadata story
regardless of which decode path they use.

One instance per decode call. Ractor-safe.

Defined Under Namespace

Classes: ReadState

Constant Summary collapse

SIGNATURE_BYTES =

PNG file signature: 8 bytes.

[137, 80, 78, 71, 13, 10, 26, 10].freeze

Instance Method Summary collapse

Constructor Details

#initialize(png, pixel_format: 'RGBA', bit_depth: 8) ⇒ StandardDecoder

Returns a new instance of StandardDecoder.

Raises:



47
48
49
50
51
52
53
54
55
56
57
# File 'lib/libpng/standard_decoder.rb', line 47

def initialize(png, pixel_format: 'RGBA', bit_depth: 8)
  raise Error, 'input PNG buffer is nil' if png.nil?
  raise Error, 'input PNG buffer must be a String' unless png.is_a?(String)
  raise Error, 'input is too short to be a PNG' if png.bytesize < 8
  raise Error, 'input is not a PNG (bad signature)' unless png.bytes.first(8) == SIGNATURE_BYTES

  @png = png
  @pixel_format_sym = coerce_pixel_format(pixel_format)
  @target_bit_depth = bit_depth
  raise Error, 'bit_depth must be 8 or 16' unless [8, 16].include?(@target_bit_depth)
end

Instance Method Details

#callObject

Returns a Libpng::DecodedImage.



60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# File 'lib/libpng/standard_decoder.rb', line 60

def call
  output = String.new.force_encoding('ASCII-8BIT')
  state = ReadState.new(bytes: @png, offset: 0)
  read_fn = make_read_fn(state)
  error_fn = make_error_fn

  png_ptr = FFI::Pointer.new(0)
  info_ptr = FFI::Pointer.new(0)
  begin
    png_ptr = Libpng::Binding.png_create_read_struct(LIBPNG_VER_STRING_C, nil, error_fn, nil)
    raise Error, 'png_create_read_struct returned NULL' if png_ptr.null?

    info_ptr = Libpng::Binding.png_create_info_struct(png_ptr)
    raise Error, 'png_create_info_struct returned NULL' if info_ptr.null?

    # Read callback + flush (NULL); we read from memory, not a file.
    Libpng::Binding.png_set_read_fn(png_ptr, nil, read_fn, nil)

    Libpng::Binding.png_read_info(png_ptr, info_ptr)
    width = Libpng::Binding.png_get_image_width(png_ptr, info_ptr)
    height = Libpng::Binding.png_get_image_height(png_ptr, info_ptr)
    src_bit_depth = Libpng::Binding.png_get_bit_depth(png_ptr, info_ptr)
    src_color_type = Libpng::Binding.png_get_color_type(png_ptr, info_ptr)

    apply_transforms(png_ptr, info_ptr, src_bit_depth, src_color_type)
    # png_set_interlace_handling returns the pass count (1 for
    # non-interlaced, 7 for Adam7). png_read_image handles the
    # multi-pass accumulation automatically once this is set.
    Libpng::Binding.png_set_interlace_handling(png_ptr)
    Libpng::Binding.png_read_update_info(png_ptr, info_ptr)

    row_bytes = Libpng::Binding.png_get_rowbytes(png_ptr, info_ptr)
    out_size = row_bytes * height

    FFI::MemoryPointer.new(:uint8, out_size) do |out_buf|
      FFI::MemoryPointer.new(:pointer, height) do |rows|
        height.times do |y|
          rows.put_pointer(y * FFI.type_size(:pointer), out_buf + (y * row_bytes))
        end
        Libpng::Binding.png_read_image(png_ptr, rows)
      end
      output << out_buf.read_bytes(out_size)
    end

    Libpng::Binding.png_read_end(png_ptr, info_ptr)

     = 
    DecodedImage.new(
      width: width,
      height: height,
      format: @pixel_format_sym.upcase.to_s,
      pixels: output,
      bit_depth: [:bit_depth],
      color_type: [:color_type],
      interlace: [:interlace],
      text: [:text],
      color: [:color],
      phys: [:phys]
    )
  ensure
    destroy(png_ptr, info_ptr)
  end
end