Class: Everywhere::Raster
- Inherits:
-
Object
- Object
- Everywhere::Raster
- Defined in:
- lib/everywhere/raster.rb
Overview
A mutable RGBA raster and the handful of pixel operations the icon generator needs — decode/encode PNG, high-quality resampling, pasting, and a superellipse ("squircle") alpha mask. Pure Ruby (zlib only): no ImageMagick, no sips, no Tauri. Works the same on any OS the CLI runs on.
Pixels are stored as a flat 8-bit RGBA byte string (row-major, 4 bytes per pixel). Resampling is a separable area/box filter done in premultiplied alpha so transparent regions never bleed their colour into opaque edges.
Instance Attribute Summary collapse
-
#height ⇒ Object
readonly
Returns the value of attribute height.
-
#pixels ⇒ Object
readonly
Returns the value of attribute pixels.
-
#width ⇒ Object
readonly
Returns the value of attribute width.
Class Method Summary collapse
-
.axis_weights(n_in, n_out) ⇒ Object
For each output index along an axis of
n_outpixels, the list of [input_index, weight] contributions, where weights sum to 1 (area filter: each output pixel is the average of the input pixels it overlaps). -
.decode_png(path) ⇒ Object
Decode a PNG file into a Raster, or nil if it's a flavor PNG.decode_rgba can't read (palette, 16-bit, interlaced, grayscale).
-
.transparent(width, height) ⇒ Object
A fully transparent canvas.
Instance Method Summary collapse
- #encode_png ⇒ Object
-
#flatten_onto(rgb) ⇒ Object
A copy composited over an opaque background color ([r, g, b] bytes) — straight alpha blend, result fully opaque.
-
#initialize(width, height, pixels = nil) ⇒ Raster
constructor
A new instance of Raster.
-
#mask_region!(x0, y0, x1, y1) ⇒ Object
Multiply the alpha channel of every pixel in [x0, x1) x [y0, y1) by the coverage the block returns for pixel (x, y) — a value in 0.0..1.0.
-
#paste!(src, ox, oy) ⇒ Object
Copy src into this raster with its top-left at (ox, oy), replacing pixels (the canvas is transparent underneath, so a plain copy is the composite).
-
#resample(new_width, new_height) ⇒ Object
Resample to a new size with a separable area filter.
- #write_png(path) ⇒ Object
Constructor Details
#initialize(width, height, pixels = nil) ⇒ Raster
Returns a new instance of Raster.
17 18 19 20 21 |
# File 'lib/everywhere/raster.rb', line 17 def initialize(width, height, pixels = nil) @width = width @height = height @pixels = pixels || ("\x00".b * (width * height * 4)) end |
Instance Attribute Details
#height ⇒ Object (readonly)
Returns the value of attribute height.
15 16 17 |
# File 'lib/everywhere/raster.rb', line 15 def height @height end |
#pixels ⇒ Object (readonly)
Returns the value of attribute pixels.
15 16 17 |
# File 'lib/everywhere/raster.rb', line 15 def pixels @pixels end |
#width ⇒ Object (readonly)
Returns the value of attribute width.
15 16 17 |
# File 'lib/everywhere/raster.rb', line 15 def width @width end |
Class Method Details
.axis_weights(n_in, n_out) ⇒ Object
For each output index along an axis of n_out pixels, the list of
[input_index, weight] contributions, where weights sum to 1 (area filter:
each output pixel is the average of the input pixels it overlaps).
147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 |
# File 'lib/everywhere/raster.rb', line 147 def self.axis_weights(n_in, n_out) scale = n_in.to_f / n_out Array.new(n_out) do |i| start = i * scale finish = [start + scale, n_in.to_f].min # float slop can push this past n_in weights = [] k = [start.floor, 0].max while k < finish && k < n_in lo = [k.to_f, start].max hi = [k + 1.0, finish].min overlap = hi - lo weights << [k, overlap / scale] if overlap > 0 k += 1 end weights end end |
.decode_png(path) ⇒ Object
Decode a PNG file into a Raster, or nil if it's a flavor PNG.decode_rgba can't read (palette, 16-bit, interlaced, grayscale).
47 48 49 50 51 52 |
# File 'lib/everywhere/raster.rb', line 47 def self.decode_png(path) width, height, bytes = PNG.decode_rgba(path) return nil unless width new(width, height, bytes) end |
.transparent(width, height) ⇒ Object
A fully transparent canvas.
24 25 26 |
# File 'lib/everywhere/raster.rb', line 24 def self.transparent(width, height) new(width, height) end |
Instance Method Details
#encode_png ⇒ Object
54 55 56 |
# File 'lib/everywhere/raster.rb', line 54 def encode_png PNG.encode_rgba(width, height, pixels) end |
#flatten_onto(rgb) ⇒ Object
A copy composited over an opaque background color ([r, g, b] bytes) — straight alpha blend, result fully opaque. Flattens icons for stores that reject alpha channels (the iOS App Store).
31 32 33 34 35 36 37 38 39 40 41 42 43 |
# File 'lib/everywhere/raster.rb', line 31 def flatten_onto(rgb) out = "\x00".b * (width * height * 4) (width * height).times do |p| i = p * 4 a = pixels.getbyte(i + 3) 3.times do |c| out.setbyte(i + c, a == 255 ? pixels.getbyte(i + c) : ((pixels.getbyte(i + c) * a + rgb[c] * (255 - a)) / 255.0).round) end out.setbyte(i + 3, 255) end self.class.new(width, height, out) end |
#mask_region!(x0, y0, x1, y1) ⇒ Object
Multiply the alpha channel of every pixel in [x0, x1) x [y0, y1) by the coverage the block returns for pixel (x, y) — a value in 0.0..1.0. Pixels outside the range keep their (already transparent) alpha.
127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 |
# File 'lib/everywhere/raster.rb', line 127 def mask_region!(x0, y0, x1, y1) y0.upto(y1 - 1) do |y| base = y * width x0.upto(x1 - 1) do |x| cov = yield(x, y) next if cov >= 1.0 idx = (base + x) * 4 + 3 a = pixels.getbyte(idx) next if a.zero? pixels.setbyte(idx, cov <= 0.0 ? 0 : (a * cov).round) end end self end |
#paste!(src, ox, oy) ⇒ Object
Copy src into this raster with its top-left at (ox, oy), replacing pixels (the canvas is transparent underneath, so a plain copy is the composite).
115 116 117 118 119 120 121 122 |
# File 'lib/everywhere/raster.rb', line 115 def paste!(src, ox, oy) row = src.width * 4 src.height.times do |y| dst_index = ((oy + y) * width + ox) * 4 pixels[dst_index, row] = src.pixels.byteslice(y * row, row) end self end |
#resample(new_width, new_height) ⇒ Object
Resample to a new size with a separable area filter. Great for the downscales an icon set is made of; degrades gracefully on upscale.
64 65 66 67 68 69 70 71 72 73 74 75 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 105 106 107 108 109 110 111 |
# File 'lib/everywhere/raster.rb', line 64 def resample(new_width, new_height) return self if new_width == width && new_height == height # Premultiplied float channels: pr = r * (a/255), etc. pr = Array.new(width * height) pg = Array.new(width * height) pb = Array.new(width * height) pa = Array.new(width * height) i = 0 (width * height).times do |p| a = pixels.getbyte(i + 3) af = a / 255.0 pr[p] = pixels.getbyte(i) * af pg[p] = pixels.getbyte(i + 1) * af pb[p] = pixels.getbyte(i + 2) * af pa[p] = a.to_f i += 4 end # Horizontal pass (width -> new_width), then vertical (height -> new_height). wx = self.class.axis_weights(width, new_width) hr = resample_axis(pr, width, height, new_width, wx, rows: true) hg = resample_axis(pg, width, height, new_width, wx, rows: true) hb = resample_axis(pb, width, height, new_width, wx, rows: true) ha = resample_axis(pa, width, height, new_width, wx, rows: true) wy = self.class.axis_weights(height, new_height) vr = resample_axis(hr, new_width, height, new_height, wy, rows: false) vg = resample_axis(hg, new_width, height, new_height, wy, rows: false) vb = resample_axis(hb, new_width, height, new_height, wy, rows: false) va = resample_axis(ha, new_width, height, new_height, wy, rows: false) out = String.new(capacity: new_width * new_height * 4) (new_width * new_height).times do |p| a = va[p] if a <= 0.001 out << "\x00\x00\x00\x00".b else out << [ (vr[p] * 255.0 / a).round.clamp(0, 255), (vg[p] * 255.0 / a).round.clamp(0, 255), (vb[p] * 255.0 / a).round.clamp(0, 255), a.round.clamp(0, 255) ].pack("C4") end end self.class.new(new_width, new_height, out) end |
#write_png(path) ⇒ Object
58 59 60 |
# File 'lib/everywhere/raster.rb', line 58 def write_png(path) File.binwrite(path, encode_png) end |