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
-
.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.
56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
# File 'lib/everywhere/png.rb', line 56 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
106 107 108 |
# File 'lib/everywhere/png.rb', line 106 def chunk(type, payload) [payload.bytesize].pack("N") + type + payload + [Zlib.crc32(type + payload)].pack("N") 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
42 43 44 45 46 47 48 49 50 51 52 53 |
# File 'lib/everywhere/png.rb', line 42 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
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 |
# File 'lib/everywhere/png.rb', line 76 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 |