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

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