Module: Pdfrb::ImageLoader::PNG

Defined in:
lib/pdfrb/image_loader/png.rb

Class Method Summary collapse

Class Method Details

.call(document, data, **_opts) ⇒ Object



8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# File 'lib/pdfrb/image_loader/png.rb', line 8

def call(document, data, **_opts)
  data = data.read if data.is_a?(IO) || data.is_a?(StringIO)
  info = parse_header(data)
  return nil if info.empty?

  dict = {
    Type: :XObject,
    Subtype: :Image,
    Width: info[:width],
    Height: info[:height],
    ColorSpace: info[:color_space],
    BitsPerComponent: info[:bits],
    Filter: :FlateDecode,
    DecodeParms: { Predictor: 15, Columns: info[:width] },
    Length: 0,
  }
  mask = color_key_mask(info)
  dict[:Mask] = mask if mask

  image = document.add(dict, type: Pdfrb::Model::Type::XObjectImage)
  image.stream = ""
  image
end

.color_key_mask(info) ⇒ Object

Build a PDF /Mask color-key array from the tRNS chunk. Each entry is [min, max] per channel. Returns nil if tRNS is absent or for indexed color (which uses /SMask instead).



81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/pdfrb/image_loader/png.rb', line 81

def color_key_mask(info)
  trns = info[:trns]
  return nil unless trns
  return nil if info[:color_type] == 3 # palette uses /SMask

  case info[:color_type]
  when 0 # grayscale: 1 two-byte value
    v = read_u16(trns, 0)
    [v, v]
  when 2, 6 # RGB: 3 two-byte values
    r = read_u16(trns, 0)
    g = read_u16(trns, 2)
    b = read_u16(trns, 4)
    [r, r, g, g, b, b]
  when 4 # grayscale+alpha: same as grayscale
    v = read_u16(trns, 0)
    [v, v]
  end
end

.parse_header(data) ⇒ Object



32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/pdfrb/image_loader/png.rb', line 32

def parse_header(data)
  return {} unless data.is_a?(::String) && data.bytesize >= 24
  return {} unless data.start_with?("\x89PNG\r\n\x1A\n".b)
  return {} unless data.bytes[12, 4].pack("C*") == "IHDR"

  off = 16
  color_type = data.getbyte(off + 9)
  result = {
    width: read_u32(data, off),
    height: read_u32(data, off + 4),
    bits: data.getbyte(off + 8),
    color_type: color_type,
    color_space: case color_type
                 when 0, 4 then :DeviceGray
                 when 2, 6 then :DeviceRGB
                 when 3 then :Indexed
                 else :DeviceRGB
                 end,
  }
  result[:trns] = read_trns_chunk(data, color_type)
  result
end

.read_trns_chunk(data, color_type) ⇒ Object

Walk the PNG chunks looking for tRNS. Returns the raw tRNS payload bytes, or nil if absent. Stops at IDAT to avoid scanning the whole file.



58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/pdfrb/image_loader/png.rb', line 58

def read_trns_chunk(data, color_type)
  offset = 8 # skip PNG signature
  while offset + 8 <= data.bytesize
    length = read_u32(data, offset)
    chunk_type = data.byteslice(offset + 4, 4)
    offset += 8
    case chunk_type
    when "IHDR" # already parsed; skip
      offset += length + 4
    when "tRNS"
      return data.byteslice(offset, length)
    when "IDAT", "IEND"
      return nil
    else
      offset += length + 4
    end
  end
  nil
end

.read_u16(data, off) ⇒ Object



101
102
103
104
105
# File 'lib/pdfrb/image_loader/png.rb', line 101

def read_u16(data, off)
  return 0 unless data && data.bytesize >= off + 2

  (data.getbyte(off) << 8) | data.getbyte(off + 1)
end

.read_u32(data, off) ⇒ Object



107
108
109
110
# File 'lib/pdfrb/image_loader/png.rb', line 107

def read_u32(data, off)
  (data.getbyte(off) << 24) | (data.getbyte(off + 1) << 16) |
    (data.getbyte(off + 2) << 8) | data.getbyte(off + 3)
end