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}