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
- .apply_filter(filter, line, prev, bpp) ⇒ Object
- .decode(bytes) ⇒ Object
- .paeth(a, b, c) ⇒ Object
-
.palette_index(row, x, depth) ⇒ Object
Index into a palette scanline, honouring sub-byte bit depths.
- .sample(row, x, depth, color_type, plte, trns) ⇒ Object
- .to_rgba(rows, width, height, depth, color_type, plte, trns) ⇒ Object
-
.unfilter(raw, width, height, depth, channels) ⇒ Object
Reverse the per-scanline PNG filters, returning one byte string per row.
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 |