code wiki / _hdl_build / nx_galx_thumb_oneshot.nx
nx_galx_thumb_oneshot.nx source
↩ module page · 122 lines · 7722 B
1// nx_galx_thumb_oneshot.nx -- SOVEREIGN gallery THUMBNAIL generator (kills full-res-into-160px-cells waste).
2// WARNING: THIS IS *NOT* nx_galx_thumb.nx. Separate organ, and it IS the binary deployed at the gallery root
3// as /volume1/ai/galx/nx_galx_thumb.elf (sha 0b760f5b, 100856 B). The FILENAME collides and this HEADER used
4// to collide as well, which has now caused the same misdiagnosis at least twice -- see the
5// nx_galx_thumb.elf.prev-batch-mistake fossil sitting beside it. The two organs are NOT interchangeable:
6// this (oneshot) : argv = <src> <out> [maxdim=384] POSITIONAL ; emits BASELINE COLOR JPEG q72
7// nx_galx_thumb : argv = one|batch SUBCOMMAND ; emits PNG at a hardcoded TH_MAX=256
8// MEASURED 2026-08-31, one image: this = 18892 B JPEG at 384 ; nx_galx_thumb one = 113861 B PNG at 256,
9// i.e. 6.03x LARGER at LOWER resolution -- a regression in the exact metric this organ exists to optimise.
10// All three consumers (nx_gallery_serve gs_thumb_run, nx_media_server ms_thumb_run,
11// nx_media_ingest_pipeline mp_run) pass the POSITIONAL form and write <cid>.jpg served as image/jpeg.
12// Installing nx_galx_thumb here BREAKS thumbnails: it exits 2 unknown mode, the rename never happens, and
13// the grid silently degrades to full-res. Verified by running both binaries, 2026-08-31.
14//
15// The gallery serves /img/<cid> = the FULL-RESOLUTION image into the grid (nx_gallery_serve.nx:2
16// "no thumbnails") -> a grid page downloads ~60 multi-MB images = the dominant "performance is awful".
17// World-class galleries NEVER serve full-res into a grid (research: thumbnail pyramids / responsive
18// variants). This composes PROVEN sovereign organs (DRY #15, zero 3rd-party): nx_png_decode ->
19// nx_img_scale_bilinear (the nc=3 path nx_img_convert already ships) -> nx_png_write_rgb. One-shot CLI;
20// the gallery generates-on-first-view + caches, so each thumbnail is computed once.
21//
22// usage: nx_galx_thumb <src.png> <out.png> [maxdim=384] (aspect-preserving, never upscales)
23// expect_exit: 0 ok ; nonzero = unreadable/undecodable/unsupported (caller falls back to full-res)
24// license_tier: ORIGINAL
25import "nx_syscalls.nx"
26import "nx_png_decoder.nx"
27import "nx_img_scale.nx"
28import "nx_png_write.nx"
29import "nx_jpeg_color_write.nx" // jcw_write_color (baseline color JPEG)
30import "nx_quant_table.nx" // nx_qt_luma / nx_qt_chroma
31import "nx_jpeg_huff_enc.nx" // jhe_* (luma Huffman tables + code gen)
32import "nx_zigzag.nx" // nx_zigzag_init
33import "nx_dct8.nx" // nx_dct8_init
34const K_MAGIC_8192: i64 = 8192
35
36func gt_puts(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(2, s, n); return 0 }
37func gt_atoi(s: *u8) -> i64 { var v: i64 = 0; var i: i64 = 0; while s[i] != (0 as u8) { let c: i64 = s[i] as i64; if c < 48 { return v } if c > 57 { return v } v = v * 10 + (c - 48); i = i + 1 } return v }
38// rule-18: log the FAILING PATH + the numeric error code so the remaining decode-error slice is MEASURABLE
39// (bucket by code: unsupported-depth vs interlace vs corrupt vs bad-CRC). A count is worthless without WHY.
40func gt_num(v: i64) -> i64 { let b: *u8=sys_mmap(28); var m: i64=v; var neg9: i64=0; if m<0{m=0-m;neg9=1}; let t: *u8=sys_mmap(28); var k: i64=0; if m==0{t[0]=48;k=1}; while m>0{t[k]=(48+(m%10)) as u8;m=m/10;k=k+1} if neg9==1{t[k]=45 as u8;k=k+1}; var i: i64=0; while i<k{b[i]=t[k-1-i];i=i+1}; sys_write(2,b,k); return 0 }
41
42func main(argc: i64, argv: *i64) -> i64 {
43 if argc < 3 { gt_puts("usage: nx_galx_thumb <src.png> <out.png> [maxdim]\n" as *u8); return 2 }
44 let src: *u8 = argv[1] as *u8
45 let out: *u8 = argv[2] as *u8
46 var maxdim: i64 = 384
47 if argc > 3 { let m: i64 = gt_atoi(argv[3] as *u8); if m > 0 { maxdim = m } }
48
49 // one-shot process (no fork-loop) -> sys_read_file's big mmap is fine here
50 let szbox: *i64 = sys_mmap(16) as *i64
51 let data: *u8 = sys_read_file(src, szbox)
52 if (data as i64) == 0 { gt_puts("thumb: cannot read src src=" as *u8); gt_puts(src); gt_puts("\n" as *u8); return 1 }
53
54 let res: *NxPngResult = nx_png_decode(data, szbox[0])
55 if (res as i64) == 0 { gt_puts("thumb: decode null src=" as *u8); gt_puts(src); gt_puts("\n" as *u8); return 3 }
56 if res.error_code != 0 { gt_puts("thumb: decode error src=" as *u8); gt_puts(src); gt_puts(" code=" as *u8); gt_num(res.error_code); gt_puts("\n" as *u8); return 4 }
57 let w: i64 = res.header.width
58 let h: i64 = res.header.height
59 let nc: i64 = res.n_channels
60 if w < 1 { return 5 }
61 if h < 1 { return 5 }
62
63 // target dims: longest side -> maxdim, aspect-preserving, never upscale
64 var dw: i64 = w
65 var dh: i64 = h
66 if w >= h {
67 if w > maxdim { dw = maxdim; dh = (h * maxdim) / w }
68 } else {
69 if h > maxdim { dh = maxdim; dw = (w * maxdim) / h }
70 }
71 if dw < 1 { dw = 1 }
72 if dh < 1 { dh = 1 }
73 // round dims DOWN to a multiple of 8 (baseline JPEG encodes whole 8x8 blocks); min 8. <=7px loss, imperceptible.
74 dw = (dw / 8) * 8; if dw < 8 { dw = 8 }
75 dh = (dh / 8) * 8; if dh < 8 { dh = 8 }
76
77 // normalize the decoded pixels to 3-channel RGB BEFORE scaling (scale at nc=3 = the proven path).
78 var src3: *u8 = res.pixels
79 if nc != 3 {
80 src3 = sys_mmap(w * h * 3 + 16)
81 var i: i64 = 0
82 let npx: i64 = w * h
83 while i < npx {
84 let so: i64 = i * nc
85 let d3: i64 = i * 3
86 if nc == 1 {
87 let g: u8 = res.pixels[so]; src3[d3] = g; src3[d3 + 1] = g; src3[d3 + 2] = g
88 } else {
89 if nc >= 3 {
90 src3[d3] = res.pixels[so]; src3[d3 + 1] = res.pixels[so + 1]; src3[d3 + 2] = res.pixels[so + 2]
91 } else {
92 let g2: u8 = res.pixels[so]; src3[d3] = g2; src3[d3 + 1] = g2; src3[d3 + 2] = g2
93 }
94 }
95 i = i + 1
96 }
97 }
98
99 let dst: *u8 = sys_mmap(dw * dh * 3 + 16)
100 if nx_img_scale_bilinear(src3, w, h, 3, dst, dw, dh) != 0 { gt_puts("thumb: scale failed\n" as *u8); return 6 }
101 // encode a baseline COLOR JPEG (q72) -- far smaller than lossless PNG for photos. Set up the shared
102 // luma Huffman tables + luma/chroma quant + zigzag + DCT (same as the proven jpeg-write gate).
103 let dcB: *i64 = sys_mmap(20*8) as *i64; let dcV: *i64 = sys_mmap(20*8) as *i64
104 let acB: *i64 = sys_mmap(20*8) as *i64; let acV: *i64 = sys_mmap(200*8) as *i64
105 let ndc: i64 = jhe_dc_bits(dcB); jhe_dc_val(dcV); let nac: i64 = jhe_ac_bits(acB); jhe_ac_val(acV)
106 let dcCO: *i64 = sys_mmap(256*8) as *i64; let dcSI: *i64 = sys_mmap(256*8) as *i64
107 let acCO: *i64 = sys_mmap(256*8) as *i64; let acSI: *i64 = sys_mmap(256*8) as *i64
108 jhe_gen(dcB, dcV, ndc, dcCO, dcSI); jhe_gen(acB, acV, nac, acCO, acSI)
109 let qtL: *i64 = sys_mmap(64*8) as *i64; nx_qt_luma(qtL, 72)
110 let qtC: *i64 = sys_mmap(64*8) as *i64; nx_qt_chroma(qtC, 72)
111 let to_zz: *i64 = sys_mmap(64*8) as *i64; let from_zz: *i64 = sys_mmap(64*8) as *i64; nx_zigzag_init(to_zz, from_zz)
112 let DM: *i64 = sys_mmap(64*8) as *i64; nx_dct8_init(DM)
113 let scratch: *i64 = sys_mmap(64*8) as *i64; let tti: *i64 = sys_mmap(8*8) as *i64; let tto: *i64 = sys_mmap(8*8) as *i64
114 let jout: *u8 = sys_mmap(dw * dh * 3 + K_MAGIC_8192)
115 let jlen: i64 = jcw_write_color(jout, dw, dh, dst, qtL, qtC, dcB, dcV, acB, acV, dcCO, dcSI, acCO, acSI, to_zz, from_zz, DM, scratch, tti, tto)
116 if jlen <= 0 { gt_puts("thumb: jpeg encode failed\n" as *u8); return 7 }
117 let ofd: i64 = sys_openat_wr(out, 0x1a4)
118 if ofd < 0 { gt_puts("thumb: out open failed\n" as *u8); return 8 }
119 sys_write(ofd, jout, jlen); sys_close(ofd)
120 gt_puts("thumb OK\n" as *u8)
121 return 0
122}