Add a PNG writer so the demos produce actual images. Two pieces: - src/jolt/png.janet — the encoder (8-bit RGB, filter None, stored/uncompressed DEFLATE so no compressor is needed; correct CRC32 + Adler32). It lives in Janet because per-byte work in the overlay is far too slow (a byte-array aset loop is ~30s for 360k bytes, and CRC32 over even a tiny image would be worse). Janet's bit ops are 32-bit signed, so the 32-bit-unsigned arithmetic is done with plain number ops (doubles hold 2^32) plus byte-level bxor. Exposed to the overlay as janet.png/* by importing it into eval_base's module-load-env. - src/jolt/jolt/png.clj — the jolt.png overlay wrapper: image / put! / write. The overlay only produces pixels; the host encodes them in one pass. mandelbrot gets a `render` subcommand (jolt -m mandelbrot render out.png [size]) that colours count-point's escape counts; the numeric-arg bench path is untouched. Verified end to end: macOS `sips` accepts the output (so CRC/zlib are valid). png-test covers the encoder structure (signature/IHDR/IEND) and the overlay round-trip. Co-authored-by: Yogthos <yogthos@gmail.com>
104 lines
3.6 KiB
Text
104 lines
3.6 KiB
Text
# Minimal PNG encoder (the host side of `jolt.png`). 8-bit truecolour RGB
|
|
# (colour type 2), no interlace, filter 0 (None), and STORED (uncompressed)
|
|
# DEFLATE blocks — a valid zlib stream that needs no compressor. Files are larger
|
|
# than a compressed PNG but decode everywhere.
|
|
#
|
|
# Lives in Janet, not Clojure: jolt's per-op cost makes per-byte work (CRC32 over
|
|
# every byte) impractical in the overlay, so the whole encode runs here natively
|
|
# and jolt calls `write` once with a raw-RGB buffer. Exposed to jolt as
|
|
# `janet.png/...` via eval_base's module-load-env (see eval_base.janet).
|
|
#
|
|
# Janet's bit ops are 32-bit SIGNED (0xFFFFFFFF overflows), so the 32-bit
|
|
# unsigned arithmetic of CRC32/Adler32 is done with plain number ops (doubles
|
|
# hold 2^32 exactly) plus byte-level bxor, whose operands are always 0..255.
|
|
|
|
(defn- u8 [v] (% v 256)) # low byte
|
|
(defn- shr [v n] (math/floor (/ v n))) # logical shift-right by a power of two
|
|
|
|
# 32-bit xor via 4 byte-wise xors (each operand 0..255, safely in 32-bit range).
|
|
(defn- xor32 [a b]
|
|
(var r 0)
|
|
(var m 1)
|
|
(for i 0 4
|
|
(set r (+ r (* (bxor (% (shr a m) 256) (% (shr b m) 256)) m)))
|
|
(set m (* m 256)))
|
|
r)
|
|
|
|
(def- crc-table
|
|
(let [t (array/new-filled 256 0)]
|
|
(for n 0 256
|
|
(var c n)
|
|
(for _ 0 8
|
|
(set c (if (= 1 (% c 2)) (xor32 0xEDB88320 (shr c 2)) (shr c 2))))
|
|
(put t n c))
|
|
t))
|
|
|
|
(defn- crc32 [buf]
|
|
(var c 0xFFFFFFFF)
|
|
(def n (length buf))
|
|
(for i 0 n
|
|
(set c (xor32 (get crc-table (bxor (% c 256) (get buf i))) (shr c 256))))
|
|
(- 0xFFFFFFFF c)) # final xor with all-ones = complement
|
|
|
|
(defn- adler32 [buf]
|
|
(var a 1)
|
|
(var b 0)
|
|
(def n (length buf))
|
|
(for i 0 n
|
|
(set a (% (+ a (get buf i)) 65521))
|
|
(set b (% (+ b a) 65521)))
|
|
(+ (* b 65536) a))
|
|
|
|
(defn- push-u32be [out v]
|
|
(buffer/push-byte out (% (shr v 16777216) 256) (% (shr v 65536) 256)
|
|
(% (shr v 256) 256) (% v 256)))
|
|
|
|
(defn- push-chunk [out typ data]
|
|
(push-u32be out (length data))
|
|
(def tagged (buffer typ data)) # CRC covers type + data
|
|
(buffer/push-string out tagged)
|
|
(push-u32be out (crc32 tagged)))
|
|
|
|
# zlib stream of `raw` as stored DEFLATE blocks (<=65535 bytes each).
|
|
(defn- zlib-store [raw]
|
|
(def out (buffer/new (+ (length raw) 64)))
|
|
(buffer/push-byte out 0x78 0x01) # zlib header: CM=8 CINFO=7, FCHECK ok
|
|
(def n (length raw))
|
|
(var pos 0)
|
|
(while (< pos n)
|
|
(def len (min 65535 (- n pos)))
|
|
(def final (if (>= (+ pos len) n) 1 0))
|
|
(def nlen (- 65535 len)) # one's-complement of len in 16 bits
|
|
(buffer/push-byte out final (% len 256) (shr len 256) (% nlen 256) (shr nlen 256))
|
|
(buffer/push-string out (buffer/slice raw pos (+ pos len)))
|
|
(set pos (+ pos len)))
|
|
(push-u32be out (adler32 raw))
|
|
out)
|
|
|
|
(defn encode
|
|
"Encode a w*h*3 raw-RGB buffer (row-major, top row first) as a PNG buffer."
|
|
[w h rgb]
|
|
(assert (= (length rgb) (* w h 3)) "png/encode: rgb length != w*h*3")
|
|
(def out (buffer/new (+ 64 (* w h 3))))
|
|
(buffer/push-string out "\x89PNG\r\n\x1a\n")
|
|
(def ihdr (buffer/new 13))
|
|
(push-u32be ihdr w)
|
|
(push-u32be ihdr h)
|
|
(buffer/push-byte ihdr 8 2 0 0 0) # bit depth 8, colour type 2 (RGB), …
|
|
(push-chunk out "IHDR" ihdr)
|
|
# scanlines, each prefixed with filter byte 0 (None)
|
|
(def stride (* w 3))
|
|
(def raw (buffer/new (* h (+ 1 stride))))
|
|
(for y 0 h
|
|
(buffer/push-byte raw 0)
|
|
(def off (* y stride))
|
|
(buffer/push-string raw (buffer/slice rgb off (+ off stride))))
|
|
(push-chunk out "IDAT" (zlib-store raw))
|
|
(push-chunk out "IEND" (buffer/new 0))
|
|
out)
|
|
|
|
(defn write
|
|
"Encode and write a w*h*3 raw-RGB buffer to `path` as a PNG."
|
|
[path w h rgb]
|
|
(spit path (encode w h rgb))
|
|
path)
|