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
-
#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).
130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 |
# File 'lib/everywhere/raster.rb', line 130 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).
30 31 32 33 34 35 |
# File 'lib/everywhere/raster.rb', line 30 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
37 38 39 |
# File 'lib/everywhere/raster.rb', line 37 def encode_png PNG.encode_rgba(width, height, pixels) 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.
110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 |
# File 'lib/everywhere/raster.rb', line 110 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).
98 99 100 101 102 103 104 105 |
# File 'lib/everywhere/raster.rb', line 98 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.
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 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 |
# File 'lib/everywhere/raster.rb', line 47 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
41 42 43 |
# File 'lib/everywhere/raster.rb', line 41 def write_png(path) File.binwrite(path, encode_png) end |