Module: Everywhere::PNG
- Defined in:
- lib/everywhere/png.rb
Overview
Minimal PNG surgery: Tauri requires RGBA icons, users hand us whatever their design tool exported. Converts 8-bit RGB (color type 2) to RGBA (color type 6); passes RGBA through untouched. No image libraries needed.
Constant Summary collapse
- SIGNATURE =
"\x89PNG\r\n\x1a\n".b
Class Method Summary collapse
-
.add_alpha(raw, width, height) ⇒ Object
Unfilter RGB scanlines, emit unfiltered (filter 0) RGBA scanlines.
- .chunk(type, payload) ⇒ Object
-
.decode_rgba(path) ⇒ Object
Decode an 8-bit RGB/RGBA PNG to a flat RGBA pixel buffer.
-
.encode_rgba(width, height, rgba) ⇒ Object
Encode a flat RGBA pixel buffer to PNG bytes (filter 0, single IDAT).
-
.ensure_rgba(src, dst) ⇒ Object
Writes an RGBA copy of src to dst.
- .read_chunks(data) ⇒ Object
- .unfilter!(line, prev, filter, bpp) ⇒ Object
Class Method Details
.add_alpha(raw, width, height) ⇒ Object
Unfilter RGB scanlines, emit unfiltered (filter 0) RGBA scanlines.
110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 |
# File 'lib/everywhere/png.rb', line 110 def add_alpha(raw, width, height) bpp = 3 stride = width * bpp prev = "\0".b * stride out = +"".b height.times do |y| filter = raw.getbyte(y * (stride + 1)) line = raw.byteslice(y * (stride + 1) + 1, stride).dup unfilter!(line, prev, filter, bpp) out << "\0" width.times do |x| out << line.byteslice(x * bpp, bpp) << "\xFF".b end prev = line end out end |
.chunk(type, payload) ⇒ Object
160 161 162 |
# File 'lib/everywhere/png.rb', line 160 def chunk(type, payload) [payload.bytesize].pack("N") + type + payload + [Zlib.crc32(type + payload)].pack("N") end |
.decode_rgba(path) ⇒ Object
Decode an 8-bit RGB/RGBA PNG to a flat RGBA pixel buffer. Returns [width, height, bytes] (bytes = widthheight4, no per-scanline filter byte) or nil for flavors we don't handle (palette, 16-bit, interlaced, grayscale). This is the pixel gateway the Raster/Icon generators build on.
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
# File 'lib/everywhere/png.rb', line 46 def decode_rgba(path) data = File.binread(path) return nil unless data.start_with?(SIGNATURE) chunks = read_chunks(data) ihdr = chunks.find { |type, _| type == "IHDR" }&.last or return nil width, height, depth, color, _comp, _filter, interlace = ihdr.unpack("NNC5") return nil unless depth == 8 && interlace.zero? return nil unless color == 2 || color == 6 # RGB or RGBA bpp = color == 6 ? 4 : 3 raw = Zlib::Inflate.inflate(chunks.select { |t, _| t == "IDAT" }.map(&:last).join) stride = width * bpp prev = "\0".b * stride out = String.new(capacity: width * height * 4) height.times do |y| filter = raw.getbyte(y * (stride + 1)) line = raw.byteslice(y * (stride + 1) + 1, stride).dup unfilter!(line, prev, filter, bpp) if bpp == 4 out << line else x = 0 while x < width out << line.byteslice(x * 3, 3) << "\xFF".b x += 1 end end prev = line end [width, height, out] end |
.encode_rgba(width, height, rgba) ⇒ Object
Encode a flat RGBA pixel buffer to PNG bytes (filter 0, single IDAT).
81 82 83 84 85 86 87 88 89 90 91 92 93 94 |
# File 'lib/everywhere/png.rb', line 81 def encode_rgba(width, height, rgba) stride = width * 4 raw = String.new(capacity: (stride + 1) * height) height.times do |y| raw << "\0".b # filter: None raw << rgba.byteslice(y * stride, stride) end out = SIGNATURE.dup out << chunk("IHDR", [width, height, 8, 6, 0, 0, 0].pack("NNC5")) out << chunk("IDAT", Zlib::Deflate.deflate(raw, Zlib::BEST_COMPRESSION)) out << chunk("IEND", "") out end |
.ensure_rgba(src, dst) ⇒ Object
Writes an RGBA copy of src to dst. Returns true on success, false for PNG flavors we don't handle (palette, 16-bit, interlaced...).
16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
# File 'lib/everywhere/png.rb', line 16 def ensure_rgba(src, dst) data = File.binread(src) return false unless data.start_with?(SIGNATURE) chunks = read_chunks(data) ihdr = chunks.find { |type, _| type == "IHDR" }&.last or return false width, height, depth, color, _comp, _filter, interlace = ihdr.unpack("NNC5") return false unless depth == 8 && interlace.zero? if color == 6 # already RGBA FileUtils.cp(src, dst) return true end return false unless color == 2 # 8-bit RGB raw = Zlib::Inflate.inflate(chunks.select { |t, _| t == "IDAT" }.map(&:last).join) rgba = add_alpha(raw, width, height) out = SIGNATURE.dup out << chunk("IHDR", [width, height, 8, 6, 0, 0, 0].pack("NNC5")) out << chunk("IDAT", Zlib::Deflate.deflate(rgba)) out << chunk("IEND", "") File.binwrite(dst, out) true end |
.read_chunks(data) ⇒ Object
96 97 98 99 100 101 102 103 104 105 106 107 |
# File 'lib/everywhere/png.rb', line 96 def read_chunks(data) chunks = [] pos = 8 while pos < data.bytesize length = data[pos, 4].unpack1("N") type = data[pos + 4, 4] chunks << [type, data[pos + 8, length]] pos += 12 + length break if type == "IEND" end chunks end |
.unfilter!(line, prev, filter, bpp) ⇒ Object
130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 |
# File 'lib/everywhere/png.rb', line 130 def unfilter!(line, prev, filter, bpp) stride = line.bytesize case filter when 0 # None when 1 # Sub bpp.upto(stride - 1) { |i| line.setbyte(i, (line.getbyte(i) + line.getbyte(i - bpp)) & 0xFF) } when 2 # Up 0.upto(stride - 1) { |i| line.setbyte(i, (line.getbyte(i) + prev.getbyte(i)) & 0xFF) } when 3 # Average 0.upto(stride - 1) do |i| left = i >= bpp ? line.getbyte(i - bpp) : 0 line.setbyte(i, (line.getbyte(i) + ((left + prev.getbyte(i)) >> 1)) & 0xFF) end when 4 # Paeth 0.upto(stride - 1) do |i| left = i >= bpp ? line.getbyte(i - bpp) : 0 up = prev.getbyte(i) up_left = i >= bpp ? prev.getbyte(i - bpp) : 0 p = left + up - up_left pa = (p - left).abs pb = (p - up).abs pc = (p - up_left).abs predictor = pa <= pb && pa <= pc ? left : (pb <= pc ? up : up_left) line.setbyte(i, (line.getbyte(i) + predictor) & 0xFF) end else raise Error, "unknown PNG filter #{filter}" end end |