nx_webp.nx source
↩ module page · 334 lines · 13290 B
1// nx_webp.nx -- WebP container + the VP8L DecodeImageStream orchestrator.
2//
3// This is the piece that ties nx_webp_vp8l's parts into a decoder: it reads
4// the transform stack, the colour cache, the meta-Huffman image and the N
5// Huffman groups, runs the pixel stream, then UNDOES the transforms in
6// reverse. Sub-images (transform data, the meta image, the palette) are
7// themselves image streams, so this function is recursive with level0=0 --
8// only the top level carries transforms and a meta image.
9//
10// genealogy_id: webp_lossless_bitstream_spec_2012
11// lineage_id: nx_webp_v1
12// license_tier: ORIGINAL
13
14import "nx_syscalls.nx"
15import "nx_bitstream.nx"
16import "nx_huffman.nx"
17import "nx_webp_huff.nx"
18import "nx_webp_vp8l.nx"
19import "nx_vp8_kf.nx" // lossy VP8 keyframe decoder (C3, 2026-08-05)
20
21const NX_WEBP_MAX_XF: i64 = 4
22
23// palette packing: how many pixels share a byte, as a shift
24func wb_palette_bits(n_colors: i64) -> i64 {
25 if n_colors <= 2 { return 3 }
26 if n_colors <= 4 { return 2 }
27 if n_colors <= 16 { return 1 }
28 return 0
29}
30
31// ===== Huffman group reading ======================================
32//
33// Returns a flat array of n_groups*5 tree pointers (widened to i64), or 0.
34// The five trees per group are, in stream order: green+length+cache, red,
35// blue, alpha, distance. Only the green tree's alphabet grows with the
36// colour cache.
37
38func wb_read_groups(bs: *NxBitStream, n_groups: i64, cache_bits: i64) -> *i64 {
39 let total: i64 = n_groups * NX_VP8L_GROUP_SIZE
40 let groups: *i64 = sys_mmap(total * 8 + 64) as *i64
41 var cache_size: i64 = 0
42 if cache_bits > 0 { cache_size = 1 << cache_bits }
43 let green_n: i64 = NX_VP8L_NUM_LIT + NX_VP8L_NUM_LEN + cache_size
44
45 var i: i64 = 0
46 var ok: i64 = 1
47 while i < total {
48 var alphabet: i64 = NX_VP8L_NUM_LIT
49 let slot: i64 = i % NX_VP8L_GROUP_SIZE
50 if slot == 0 { alphabet = green_n }
51 if slot == 4 { alphabet = NX_VP8L_NUM_DIST }
52 let t: *NxWlTree = wl_read_huffman_code(bs, alphabet)
53 if t == (0 as *NxWlTree) { ok = 0; i = total } else {
54 groups[i] = t as i64
55 i = i + 1
56 }
57 }
58 if ok == 0 { return 0 as *i64 }
59 return groups
60}
61
62// ===== the image stream ===========================================
63//
64// out must hold width*height i64 ARGB entries. Returns 1 on success.
65// A malformed stream returns 0 rather than a partially-filled buffer --
66// a wrong answer is worse than an absent one.
67
68func wb_decode_stream(bs: *NxBitStream, width: i64, height: i64, level0: i64,
69 plane: *i64, out: *i64) -> i64 {
70 let xf_type: *i64 = sys_mmap(NX_WEBP_MAX_XF * 8 + 64) as *i64
71 let xf_bits: *i64 = sys_mmap(NX_WEBP_MAX_XF * 8 + 64) as *i64
72 let xf_data: *i64 = sys_mmap(NX_WEBP_MAX_XF * 8 + 64) as *i64
73 let xf_ncol: *i64 = sys_mmap(NX_WEBP_MAX_XF * 8 + 64) as *i64
74 let seen: *i64 = sys_mmap(NX_WEBP_MAX_XF * 8 + 64) as *i64
75 var i: i64 = 0
76 while i < NX_WEBP_MAX_XF { seen[i] = 0; xf_data[i] = 0; xf_ncol[i] = 0; i = i + 1 }
77
78 var n_xf: i64 = 0
79 var cur_w: i64 = width
80 var ok: i64 = 1
81
82 // ---- transform stack (top level only) ----
83 if level0 == 1 {
84 var more: i64 = nx_bitstream_read_lsb(bs, 1)
85 while more == 1 {
86 if n_xf >= NX_WEBP_MAX_XF { ok = 0; more = 0 } else {
87 let t: i64 = nx_bitstream_read_lsb(bs, 2)
88 if seen[t] == 1 { ok = 0; more = 0 } else {
89 seen[t] = 1
90 xf_type[n_xf] = t
91 if t == NX_VP8L_XF_PALETTE {
92 let n_colors: i64 = nx_bitstream_read_lsb(bs, 8) + 1
93 let pal: *i64 = sys_mmap(n_colors * 8 + 64) as *i64
94 if wb_decode_stream(bs, n_colors, 1, 0, plane, pal) == 0 { ok = 0 } else {
95 // the palette is delta-coded along its single row
96 var c: i64 = 1
97 while c < n_colors {
98 let p: i64 = pal[c]
99 let q: i64 = pal[c - 1]
100 let a: i64 = (vl_chan(p, 24) + vl_chan(q, 24)) & 255
101 let r: i64 = (vl_chan(p, 16) + vl_chan(q, 16)) & 255
102 let g: i64 = (vl_chan(p, 8) + vl_chan(q, 8)) & 255
103 let b: i64 = (vl_chan(p, 0) + vl_chan(q, 0)) & 255
104 pal[c] = vl_argb(a, r, g, b)
105 c = c + 1
106 }
107 xf_data[n_xf] = pal as i64
108 xf_ncol[n_xf] = n_colors
109 let pb: i64 = wb_palette_bits(n_colors)
110 xf_bits[n_xf] = pb
111 cur_w = vl_subsample_size(cur_w, pb)
112 }
113 } else {
114 if t == NX_VP8L_XF_SUBGREEN {
115 xf_bits[n_xf] = 0
116 } else {
117 // predictor and colour transforms both carry a sub-image
118 let bits: i64 = nx_bitstream_read_lsb(bs, 3) + 2
119 let bw: i64 = vl_subsample_size(cur_w, bits)
120 let bh: i64 = vl_subsample_size(height, bits)
121 let sub: *i64 = sys_mmap(bw * bh * 8 + 64) as *i64
122 if wb_decode_stream(bs, bw, bh, 0, plane, sub) == 0 { ok = 0 } else {
123 xf_bits[n_xf] = bits
124 xf_data[n_xf] = sub as i64
125 }
126 } }
127 if ok == 0 { more = 0 } else {
128 n_xf = n_xf + 1
129 more = nx_bitstream_read_lsb(bs, 1)
130 }
131 }
132 }
133 }
134 }
135 if ok == 0 { return 0 }
136
137 // ---- colour cache ----
138 var cache_bits: i64 = 0
139 if nx_bitstream_read_lsb(bs, 1) == 1 {
140 cache_bits = nx_bitstream_read_lsb(bs, 4)
141 if cache_bits < 1 { return 0 }
142 if cache_bits > 11 { return 0 }
143 }
144
145 // ---- meta-Huffman image (top level only) ----
146 var meta: *i64 = 0 as *i64
147 var meta_bits: i64 = 0
148 var meta_w: i64 = 0
149 var n_groups: i64 = 1
150 if level0 == 1 {
151 if nx_bitstream_read_lsb(bs, 1) == 1 {
152 meta_bits = nx_bitstream_read_lsb(bs, 3) + 2
153 meta_w = vl_subsample_size(cur_w, meta_bits)
154 let mh: i64 = vl_subsample_size(height, meta_bits)
155 let raw: *i64 = sys_mmap(meta_w * mh * 8 + 64) as *i64
156 if wb_decode_stream(bs, meta_w, mh, 0, plane, raw) == 0 { return 0 }
157 // the group index is packed into the red and green bytes
158 let ncell: i64 = meta_w * mh
159 var k: i64 = 0
160 var maxg: i64 = 0
161 while k < ncell {
162 let g: i64 = (vl_chan(raw[k], 16) << 8) | vl_chan(raw[k], 8)
163 raw[k] = g
164 if g > maxg { maxg = g }
165 k = k + 1
166 }
167 meta = raw
168 n_groups = maxg + 1
169 }
170 }
171
172 // ---- Huffman groups + the pixel stream ----
173 let groups: *i64 = wb_read_groups(bs, n_groups, cache_bits)
174 if groups == (0 as *i64) { return 0 }
175
176 var pixels: *i64 = out
177 if cur_w != width {
178 // palette-packed: decode narrow, expand later
179 pixels = sys_mmap(cur_w * height * 8 + 64) as *i64
180 }
181 if vl_decode_pixels(bs, pixels, cur_w, height, groups, meta, meta_bits,
182 meta_w, cache_bits, plane) == 0 { return 0 }
183
184 // ---- inverse transforms, in REVERSE order ----
185 var x: i64 = n_xf - 1
186 while x >= 0 {
187 let t: i64 = xf_type[x]
188 if t == NX_VP8L_XF_SUBGREEN {
189 vl_inv_subtract_green(out, width * height)
190 } else {
191 if t == NX_VP8L_XF_PREDICT {
192 vl_inv_predict(out, width, height, xf_bits[x], xf_data[x] as *i64)
193 } else {
194 if t == NX_VP8L_XF_COLOR {
195 vl_inv_color(out, width, height, xf_bits[x], xf_data[x] as *i64)
196 } else {
197 // palette: expand packed indices back to full width
198 let pal: *i64 = xf_data[x] as *i64
199 let pb: i64 = xf_bits[x]
200 let ncol: i64 = xf_ncol[x]
201 let per: i64 = 1 << pb
202 let mask: i64 = (1 << (8 >> pb)) - 1
203 var y: i64 = 0
204 while y < height {
205 var px: i64 = 0
206 while px < width {
207 let packed: i64 = pixels[y * cur_w + (px >> pb)]
208 let shift: i64 = (px & (per - 1)) * (8 >> pb)
209 var idx: i64 = (vl_chan(packed, 8) >> shift) & mask
210 if idx >= ncol { idx = 0 }
211 out[y * width + px] = pal[idx]
212 px = px + 1
213 }
214 y = y + 1
215 }
216 } } }
217 x = x - 1
218 }
219 return 1
220}
221
222// ===== RIFF container =============================================
223//
224// Accepts "RIFF"<size>"WEBP" then walks chunks to VP8L OR 'VP8 ' (lossy
225// keyframe, decoded by nx_vp8_kf since 2026-08-05). A simple-format
226// lossless file is RIFF/WEBP/VP8L; an extended file is RIFF/WEBP/VP8X/.../VP8L.
227// Returns a width*height ARGB buffer, or 0. out_wh receives w,h.
228
229func wb_u32le(b: *u8, off: i64) -> i64 {
230 return ((b[off] as i64) & 255) | (((b[off+1] as i64) & 255) << 8)
231 | (((b[off+2] as i64) & 255) << 16) | (((b[off+3] as i64) & 255) << 24)
232}
233
234func wb_fourcc_is(b: *u8, off: i64, a: i64, c: i64, d: i64, e: i64) -> i64 {
235 if (b[off] as i64 & 255) != a { return 0 }
236 if (b[off+1] as i64 & 255) != c { return 0 }
237 if (b[off+2] as i64 & 255) != d { return 0 }
238 if (b[off+3] as i64 & 255) != e { return 0 }
239 return 1
240}
241
242func webp_decode(raw: *u8, n: i64, out_wh: *i64) -> *i64 {
243 if n < 20 { return 0 as *i64 }
244 if wb_fourcc_is(raw, 0, 0x52, 0x49, 0x46, 0x46) == 0 { return 0 as *i64 }
245 if wb_fourcc_is(raw, 8, 0x57, 0x45, 0x42, 0x50) == 0 { return 0 as *i64 }
246
247 // walk chunks from offset 12 to the VP8L payload
248 var pos: i64 = 12
249 var found: i64 = 0
250 var isvp8: i64 = 0
251 var payload: i64 = 0
252 var paylen: i64 = 0
253 var go: i64 = 1
254 while go == 1 {
255 if pos + 8 > n { go = 0 } else {
256 let clen: i64 = wb_u32le(raw, pos + 4)
257 if wb_fourcc_is(raw, pos, 0x56, 0x50, 0x38, 0x4c) == 1 {
258 payload = pos + 8
259 paylen = clen
260 found = 1
261 go = 0
262 } else {
263 if wb_fourcc_is(raw, pos, 0x56, 0x50, 0x38, 0x20) == 1 {
264 // 'VP8 ' -- lossy keyframe (simple or inside VP8X)
265 payload = pos + 8
266 paylen = clen
267 found = 1
268 isvp8 = 1
269 go = 0
270 } else {
271 var adv: i64 = clen
272 if (adv & 1) == 1 { adv = adv + 1 }
273 pos = pos + 8 + adv
274 } }
275 }
276 }
277 if found == 0 { return 0 as *i64 }
278 if payload + paylen > n { paylen = n - payload }
279 if paylen <= 5 { return 0 as *i64 }
280
281 // ---- lossy VP8 keyframe: sovereign C3 decoder + BT.601 YUV->ARGB ----
282 if isvp8 == 1 {
283 let vb: *i64 = sys_mmap(64) as *i64
284 if nx_vp8_kf_decode(raw + payload, paylen, vb) == 0 { return 0 as *i64 }
285 let py: *u8 = vb[NX_VP8KF_OUT_Y] as *u8
286 let pu: *u8 = vb[NX_VP8KF_OUT_U] as *u8
287 let pv: *u8 = vb[NX_VP8KF_OUT_V] as *u8
288 let vw: i64 = vb[NX_VP8KF_OUT_W]
289 let vh: i64 = vb[NX_VP8KF_OUT_H]
290 let vys: i64 = vb[NX_VP8KF_OUT_YS]
291 let vus: i64 = vb[NX_VP8KF_OUT_UVS]
292 let vargb: *i64 = sys_mmap(vw * vh * 8 + 64) as *i64
293 var vy: i64 = 0
294 while vy < vh {
295 var vx: i64 = 0
296 while vx < vw {
297 let yv: i64 = ((py[vy*vys+vx] as i64) & 255) - 16
298 let uv: i64 = ((pu[(vy>>1)*vus+(vx>>1)] as i64) & 255) - 128
299 let vv: i64 = ((pv[(vy>>1)*vus+(vx>>1)] as i64) & 255) - 128
300 var tr: i64 = 298*yv + 409*vv + 128
301 var tg: i64 = 298*yv - 100*uv - 208*vv + 128
302 var tb: i64 = 298*yv + 516*uv + 128
303 if tr < 0 { tr = 0 } else { tr = tr >> 8 }
304 if tg < 0 { tg = 0 } else { tg = tg >> 8 }
305 if tb < 0 { tb = 0 } else { tb = tb >> 8 }
306 if tr > 255 { tr = 255 }
307 if tg > 255 { tg = 255 }
308 if tb > 255 { tb = 255 }
309 vargb[vy*vw+vx] = (255 << 24) | (tr << 16) | (tg << 8) | tb
310 vx = vx + 1
311 }
312 vy = vy + 1
313 }
314 out_wh[0] = vw
315 out_wh[1] = vh
316 return vargb
317 }
318
319 let bs: *NxBitStream = nx_bitstream_alloc(raw + payload, paylen)
320 let hdr: *i64 = sys_mmap(64) as *i64
321 if vp8l_read_header(bs, hdr) == 0 { return 0 as *i64 }
322 let w: i64 = hdr[0]
323 let h: i64 = hdr[1]
324
325 let plane: *i64 = sys_mmap(NX_VP8L_PLANE_CODES * 8 + 64) as *i64
326 vl_plane_table(plane)
327
328 let argb: *i64 = sys_mmap(w * h * 8 + 64) as *i64
329 if wb_decode_stream(bs, w, h, 1, plane, argb) == 0 { return 0 as *i64 }
330
331 out_wh[0] = w
332 out_wh[1] = h
333 return argb
334}