nx_apng.nx source
↩ module page · 162 lines · 7301 B
1// nx_apng.nx -- SOVEREIGN ANIMATED PNG (APNG) encoder: our PNG codec extended with the animation-control chunks
2// (acTL / fcTL / fdAT) so a sequence of framebuffers becomes ONE browser-native looping "video" file -- no
3// 3rd-party video encoder. Each frame is filtered + DEFLATE-compressed exactly like a PNG IDAT (reuses the
4// nx_deflate_enc + the standard PNG scanline filters). This is the swap+motion S3 "show it in a VIDEO" surface;
5// the real inter-frame vcodec (nx_video_codec_wasm) is the production rung. Incremental API: apng_open ->
6// apng_frame x N -> apng_close. All integer, deterministic. license_tier: ORIGINAL
7import "nx_syscalls.nx"
8import "nx_deflate_enc.nx"
9const K_MAGIC_65521: i64 = 65521
10const K_MAGIC_65536: i64 = 65536
11
12func ap_crc32(buf: *u8, off: i64, len: i64) -> i64 {
13 var crc: i64 = 0xFFFFFFFF
14 var i: i64 = 0
15 while i < len {
16 crc = crc ^ (buf[off + i] as i64)
17 var k: i64 = 0
18 while k < 8 { let m: i64 = 0 - (crc & 1); crc = ((crc >> 1) ^ (0xEDB88320 & m)) & 0xFFFFFFFF; k = k + 1 }
19 i = i + 1
20 }
21 return (crc ^ 0xFFFFFFFF) & 0xFFFFFFFF
22}
23func ap_adler32(buf: *u8, off: i64, len: i64) -> i64 {
24 var s1: i64 = 1; var s2: i64 = 0; var i: i64 = 0
25 while i < len { s1 = (s1 + (buf[off + i] as i64)) % K_MAGIC_65521; s2 = (s2 + s1) % K_MAGIC_65521; i = i + 1 }
26 return s2 * K_MAGIC_65536 + s1
27}
28func ap_be32(b: *u8, o: i64, v: i64) -> i64 { b[o]=((v>>24)&0xff) as u8; b[o+1]=((v>>16)&0xff) as u8; b[o+2]=((v>>8)&0xff) as u8; b[o+3]=(v&0xff) as u8; return 0 }
29func ap_be16(b: *u8, o: i64, v: i64) -> i64 { b[o]=((v>>8)&0xff) as u8; b[o+1]=(v&0xff) as u8; return 0 }
30func ap_paeth(a: i64, b: i64, c: i64) -> i64 {
31 let p: i64 = a + b - c
32 var pa: i64 = p - a; if pa < 0 { pa = 0 - pa }
33 var pb: i64 = p - b; if pb < 0 { pb = 0 - pb }
34 var pc: i64 = p - c; if pc < 0 { pc = 0 - pc }
35 if pa <= pb { if pa <= pc { return a } }
36 if pb <= pc { return b }
37 return c
38}
39func ap_sabs(v: i64) -> i64 { var x: i64 = v & 0xff; if x > 127 { x = 256 - x } return x }
40func ap_g(img: *u8, w3: i64, r: i64, j: i64) -> i64 { if r < 0 { return 0 } if j < 0 { return 0 } return img[r * w3 + j] as i64 }
41
42// framebuffer (w*h packed RGB in i64) -> zlib stream into z. Returns zlen. Mirrors nx_png write_png steps 1-3
43// (the sovereign PNG path); kept local so nx_png's file writer stays untouched (2 copies, under the DRY-extract
44// threshold). z must be sized >= h*(1+w*3)*3/2 + 256.
45func ap_fb_to_zlib(fb: *i64, w: i64, h: i64, z: *u8) -> i64 {
46 let w3: i64 = w * 3
47 let img: *u8 = sys_mmap(h * w3) as *u8
48 var y: i64 = 0
49 while y < h {
50 var x: i64 = 0
51 while x < w {
52 let c: i64 = fb[y * w + x]
53 img[y * w3 + x * 3] = (c & 0xff) as u8
54 img[y * w3 + x * 3 + 1] = ((c >> 8) & 0xff) as u8
55 img[y * w3 + x * 3 + 2] = ((c >> 16) & 0xff) as u8
56 x = x + 1
57 }
58 y = y + 1
59 }
60 let rowbytes: i64 = 1 + w3
61 let rawlen: i64 = h * rowbytes
62 let raw: *u8 = sys_mmap(rawlen) as *u8
63 var o: i64 = 0
64 y = 0
65 while y < h {
66 var bestft: i64 = 0
67 var bestcost: i64 = 0 - 1
68 var ft: i64 = 0
69 while ft < 5 {
70 var cost: i64 = 0
71 var j: i64 = 0
72 while j < w3 {
73 let xb: i64 = ap_g(img, w3, y, j)
74 let left: i64 = ap_g(img, w3, y, j - 3)
75 let up: i64 = ap_g(img, w3, y - 1, j)
76 let ul: i64 = ap_g(img, w3, y - 1, j - 3)
77 var res: i64 = xb
78 if ft == 1 { res = xb - left }
79 if ft == 2 { res = xb - up }
80 if ft == 3 { res = xb - (left + up) / 2 }
81 if ft == 4 { res = xb - ap_paeth(left, up, ul) }
82 cost = cost + ap_sabs(res)
83 j = j + 1
84 }
85 if bestcost < 0 { bestcost = cost; bestft = ft } else { if cost < bestcost { bestcost = cost; bestft = ft } }
86 ft = ft + 1
87 }
88 raw[o] = bestft as u8; o = o + 1
89 var j2: i64 = 0
90 while j2 < w3 {
91 let xb: i64 = ap_g(img, w3, y, j2)
92 let left: i64 = ap_g(img, w3, y, j2 - 3)
93 let up: i64 = ap_g(img, w3, y - 1, j2)
94 let ul: i64 = ap_g(img, w3, y - 1, j2 - 3)
95 var res: i64 = xb
96 if bestft == 1 { res = xb - left }
97 if bestft == 2 { res = xb - up }
98 if bestft == 3 { res = xb - (left + up) / 2 }
99 if bestft == 4 { res = xb - ap_paeth(left, up, ul) }
100 raw[o] = (res & 0xff) as u8; o = o + 1
101 j2 = j2 + 1
102 }
103 y = y + 1
104 }
105 z[0] = 0x78 as u8; z[1] = 0x01 as u8
106 let body: *u8 = (z as i64 + 2) as *u8
107 let clen: i64 = dfe_compress(raw, rawlen, body)
108 var zo: i64 = 2 + clen
109 ap_be32(z, zo, ap_adler32(raw, 0, rawlen)); zo = zo + 4
110 return zo
111}
112
113// write a chunk: len + type(a,b,c,d) + data + crc. Returns new offset.
114func ap_chunk(p: *u8, o: i64, a: i64, b: i64, c: i64, d: i64, data: *u8, dlen: i64) -> i64 {
115 ap_be32(p, o, dlen); var w: i64 = o + 4
116 let ts: i64 = w
117 p[w] = a as u8; p[w+1] = b as u8; p[w+2] = c as u8; p[w+3] = d as u8; w = w + 4
118 var i: i64 = 0
119 while i < dlen { p[w + i] = data[i]; i = i + 1 }
120 w = w + dlen
121 ap_be32(p, w, ap_crc32(p, ts, 4 + dlen)); w = w + 4
122 return w
123}
124
125// sig + IHDR + acTL. Returns offset. num_plays=0 => loop forever.
126func apng_open(p: *u8, w: i64, h: i64, nframes: i64) -> i64 {
127 p[0]=137 as u8; p[1]=80 as u8; p[2]=78 as u8; p[3]=71 as u8; p[4]=13 as u8; p[5]=10 as u8; p[6]=26 as u8; p[7]=10 as u8
128 var o: i64 = 8
129 let ihdr: *u8 = sys_mmap(16)
130 ap_be32(ihdr, 0, w); ap_be32(ihdr, 4, h); ihdr[8]=8 as u8; ihdr[9]=2 as u8; ihdr[10]=0 as u8; ihdr[11]=0 as u8; ihdr[12]=0 as u8
131 o = ap_chunk(p, o, 73, 72, 68, 82, ihdr, 13) // IHDR
132 let actl: *u8 = sys_mmap(16)
133 ap_be32(actl, 0, nframes); ap_be32(actl, 4, 0)
134 o = ap_chunk(p, o, 97, 99, 84, 76, actl, 8) // acTL
135 return o
136}
137// one frame: fcTL (+ IDAT if first, else fdAT). seqp = running sequence counter. delay_cs = frame delay in 1/100s.
138func apng_frame(p: *u8, o: i64, seqp: *i64, fb: *i64, w: i64, h: i64, delay_cs: i64, first: i64) -> i64 {
139 let fctl: *u8 = sys_mmap(32)
140 ap_be32(fctl, 0, seqp[0]); ap_be32(fctl, 4, w); ap_be32(fctl, 8, h); ap_be32(fctl, 12, 0); ap_be32(fctl, 16, 0)
141 ap_be16(fctl, 20, delay_cs); ap_be16(fctl, 22, 100); fctl[24]=0 as u8; fctl[25]=0 as u8 // dispose=none, blend=source
142 var oo: i64 = ap_chunk(p, o, 102, 99, 84, 76, fctl, 26) // fcTL
143 seqp[0] = seqp[0] + 1
144 let zcap: i64 = h * (1 + w * 3) * 3 / 2 + 256
145 let z: *u8 = sys_mmap(zcap)
146 let zlen: i64 = ap_fb_to_zlib(fb, w, h, z)
147 if first == 1 {
148 oo = ap_chunk(p, oo, 73, 68, 65, 84, z, zlen) // IDAT (default image = frame 0)
149 } else {
150 let fd: *u8 = sys_mmap(zlen + 16)
151 ap_be32(fd, 0, seqp[0])
152 var i: i64 = 0
153 while i < zlen { fd[4 + i] = z[i]; i = i + 1 }
154 oo = ap_chunk(p, oo, 102, 100, 65, 84, fd, zlen + 4) // fdAT
155 seqp[0] = seqp[0] + 1
156 }
157 return oo
158}
159func apng_close(p: *u8, o: i64) -> i64 {
160 let e: *u8 = sys_mmap(8)
161 return ap_chunk(p, o, 73, 69, 78, 68, e, 0) // IEND
162}