Module: Skrift::Color::PNG

Defined in:
lib/skrift/color/png.rb

Overview

A small, dependency-free PNG decoder, just enough for the bitmaps found in colour-emoji fonts (CBDT/sbix). Supports 8-bit RGB (2) and RGBA (6), and palette (3, any bit depth) with a tRNS alpha table. Returns

width, height, pixels

where pixels is a row-major array of packed

0xRRGGBBAA integers. Raises on anything it doesn’t understand.

Constant Summary collapse

SIGNATURE =
"\x89PNG\r\n\x1a\n".b
CHANNELS =
{ 0 => 1, 2 => 3, 3 => 1, 4 => 2, 6 => 4 }.freeze

Class Method Summary collapse

Class Method Details

.apply_filter(filter, line, prev, bpp) ⇒ Object



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# File 'lib/skrift/color/png.rb', line 66

def apply_filter(filter, line, prev, bpp)
  bytes = line.bytes
  bytes.each_index do |i|
    a = i >= bpp ? bytes[i - bpp] : 0
    b = prev.getbyte(i)
    c = i >= bpp ? prev.getbyte(i - bpp) : 0
    add =
      case filter
      when 0 then 0
      when 1 then a
      when 2 then b
      when 3 then (a + b) / 2
      when 4 then paeth(a, b, c)
      else raise "bad PNG filter #{filter}"
      end
    bytes[i] = (bytes[i] + add) & 0xff
  end
  bytes.pack("C*")
end

.decode(bytes) ⇒ Object



17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# File 'lib/skrift/color/png.rb', line 17

def decode(bytes)
  bytes = bytes.b
  raise "not a PNG" unless bytes[0, 8] == SIGNATURE

  ihdr = nil
  idat = +"".b
  plte = nil
  trns = nil
  pos  = 8
  while pos + 8 <= bytes.bytesize
    len  = bytes[pos, 4].unpack1("N")
    type = bytes[pos + 4, 4]
    data = bytes[pos + 8, len]
    case type
    when "IHDR" then ihdr = data
    when "PLTE" then plte = data
    when "tRNS" then trns = data
    when "IDAT" then idat << data
    when "IEND" then break
    end
    pos += 12 + len # length + type + data + CRC
  end

  width, height, depth, color_type = ihdr.unpack("NNCC")
  channels = CHANNELS.fetch(color_type) { raise "unsupported PNG colour type #{color_type}" }
  rows = unfilter(Zlib::Inflate.inflate(idat), width, height, depth, channels)
  to_rgba(rows, width, height, depth, color_type, plte, trns)
end

.paeth(a, b, c) ⇒ Object



86
87
88
89
90
# File 'lib/skrift/color/png.rb', line 86

def paeth(a, b, c)
  p = a + b - c
  pa = (p - a).abs; pb = (p - b).abs; pc = (p - c).abs
  pa <= pb && pa <= pc ? a : (pb <= pc ? b : c)
end

.palette_index(row, x, depth) ⇒ Object

Index into a palette scanline, honouring sub-byte bit depths.



123
124
125
126
127
128
129
# File 'lib/skrift/color/png.rb', line 123

def palette_index(row, x, depth)
  return row.getbyte(x) if depth == 8
  per_byte = 8 / depth
  byte = row.getbyte(x / per_byte)
  shift = (per_byte - 1 - (x % per_byte)) * depth
  (byte >> shift) & ((1 << depth) - 1)
end

.sample(row, x, depth, color_type, plte, trns) ⇒ Object



104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/skrift/color/png.rb', line 104

def sample(row, x, depth, color_type, plte, trns)
  case color_type
  when 6 # RGBA, 8-bit
    o = x * 4
    [row.getbyte(o), row.getbyte(o + 1), row.getbyte(o + 2), row.getbyte(o + 3)]
  when 2 # RGB, 8-bit
    o = x * 3
    [row.getbyte(o), row.getbyte(o + 1), row.getbyte(o + 2), 255]
  when 3 # palette
    idx = palette_index(row, x, depth)
    po  = idx * 3
    a   = trns && idx < trns.bytesize ? trns.getbyte(idx) : 255
    [plte.getbyte(po), plte.getbyte(po + 1), plte.getbyte(po + 2), a]
  else
    raise "unsupported PNG colour type #{color_type}"
  end
end

.to_rgba(rows, width, height, depth, color_type, plte, trns) ⇒ Object



92
93
94
95
96
97
98
99
100
101
102
# File 'lib/skrift/color/png.rb', line 92

def to_rgba(rows, width, height, depth, color_type, plte, trns)
  pixels = Array.new(width * height, 0)
  height.times do |y|
    row = rows[y]
    width.times do |x|
      r, g, b, a = sample(row, x, depth, color_type, plte, trns)
      pixels[y * width + x] = (r << 24) | (g << 16) | (b << 8) | a
    end
  end
  [width, height, pixels]
end

.unfilter(raw, width, height, depth, channels) ⇒ Object

Reverse the per-scanline PNG filters, returning one byte string per row.



49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/skrift/color/png.rb', line 49

def unfilter(raw, width, height, depth, channels)
  bpp    = [(depth * channels) / 8, 1].max          # bytes per pixel (>=1)
  stride = (width * channels * depth + 7) / 8        # bytes per row
  rows   = []
  prev   = ("\0" * stride).b
  pos    = 0
  height.times do
    filter = raw.getbyte(pos)
    line   = raw.byteslice(pos + 1, stride).dup
    recon  = apply_filter(filter, line, prev, bpp)
    rows << recon
    prev = recon
    pos += 1 + stride
  end
  rows
end