nx_escl.nx source
↩ module page · 108 lines · 5418 B
1// nx_escl.nx -- sovereign eSCL (AirScan / Mopria Scan) protocol layer -- R-SCAN R0, the "scan from ANY
2// scanner" rung. eSCL is to scanners what IPP is to printers: a vendor-neutral HTTP + XML standard that Apple
3// AirScan and Mopria Scan use, and that modern Brother/HP/Canon/Epson MFPs answer. This file is the pure
4// protocol layer:
5// - EMIT a ScanSettings XML document (the POST body for /eSCL/ScanJobs): resolution, color mode, region,
6// input source, document format.
7// - PARSE the key fields from a ScannerCapabilities XML response (GET /eSCL/ScannerCapabilities): MaxWidth,
8// MaxHeight, supported color modes + document formats, via targeted tag extraction (no full XML parser).
9//
10// Live transport (GET caps -> POST ScanJobs -> GET NextDocument over HTTP) + the JPEG decode are the next
11// rungs (R-SCAN R1/R2). never-brick #26: pure byte emit/parse, the only syscall is sys_mmap for an int-format
12// scratch. no_silent_failure: an absent tag yields -1, a missing format yields 0 -- never a fabricated value.
13//
14// genealogy_id: project-printer-management-ipp-sclass-2026-06-20 ; license_tier: ORIGINAL
15// CITED: eSCL/Mopria scan uses the PWG SM + HP eSCL schemas; XML uses single-quoted attrs here to stay
16// literal-clean. Namespace prefixes scan:/pwg: are the common emission; prefix-robust parsing is a follow-on.
17
18import "nx_syscalls.nx"
19
20func ex_puts(buf: *u8, off: i64, s: *u8) -> i64 {
21 var o: i64 = off
22 var i: i64 = 0
23 while s[i] != (0 as u8) { buf[o] = s[i]; o = o + 1; i = i + 1 }
24 return o
25}
26
27func ex_puti(buf: *u8, off: i64, v: i64) -> i64 {
28 if v == 0 { buf[off] = 48 as u8; return off + 1 }
29 var o: i64 = off
30 var m: i64 = v
31 let t: *u8 = sys_mmap(24)
32 var k: i64 = 0
33 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 }
34 var i: i64 = k - 1
35 while i >= 0 { buf[o] = t[i]; o = o + 1; i = i - 1 }
36 return o
37}
38
39// Emit an eSCL ScanSettings document. color/fmt/source are NUL-terminated eSCL tokens
40// (e.g. color="Grayscale8"/"RGB24", fmt="image/jpeg"/"application/pdf", source="Platen"/"Feeder").
41// Returns the byte length written.
42func nx_escl_scan_settings(buf: *u8, res: i64, w: i64, h: i64, color: *u8, fmt: *u8, source: *u8) -> i64 {
43 var o: i64 = 0
44 o = ex_puts(buf, o, "<?xml version='1.0' encoding='UTF-8'?>\n" as *u8)
45 o = ex_puts(buf, o, "<scan:ScanSettings xmlns:pwg='http://www.pwg.org/schemas/2010/12/sm' xmlns:scan='http://schemas.hp.com/imaging/escl/2011/05/03'>\n" as *u8)
46 o = ex_puts(buf, o, "<pwg:Version>2.6</pwg:Version>\n" as *u8)
47 o = ex_puts(buf, o, "<scan:Intent>Document</scan:Intent>\n" as *u8)
48 o = ex_puts(buf, o, "<pwg:ScanRegions><pwg:ScanRegion>" as *u8)
49 o = ex_puts(buf, o, "<pwg:Height>" as *u8); o = ex_puti(buf, o, h); o = ex_puts(buf, o, "</pwg:Height>" as *u8)
50 o = ex_puts(buf, o, "<pwg:Width>" as *u8); o = ex_puti(buf, o, w); o = ex_puts(buf, o, "</pwg:Width>" as *u8)
51 o = ex_puts(buf, o, "<pwg:XOffset>0</pwg:XOffset><pwg:YOffset>0</pwg:YOffset>" as *u8)
52 o = ex_puts(buf, o, "</pwg:ScanRegion></pwg:ScanRegions>\n" as *u8)
53 o = ex_puts(buf, o, "<scan:InputSource>" as *u8); o = ex_puts(buf, o, source); o = ex_puts(buf, o, "</scan:InputSource>\n" as *u8)
54 o = ex_puts(buf, o, "<scan:ColorMode>" as *u8); o = ex_puts(buf, o, color); o = ex_puts(buf, o, "</scan:ColorMode>\n" as *u8)
55 o = ex_puts(buf, o, "<scan:XResolution>" as *u8); o = ex_puti(buf, o, res); o = ex_puts(buf, o, "</scan:XResolution>\n" as *u8)
56 o = ex_puts(buf, o, "<scan:YResolution>" as *u8); o = ex_puti(buf, o, res); o = ex_puts(buf, o, "</scan:YResolution>\n" as *u8)
57 o = ex_puts(buf, o, "<pwg:DocumentFormat>" as *u8); o = ex_puts(buf, o, fmt); o = ex_puts(buf, o, "</pwg:DocumentFormat>\n" as *u8)
58 o = ex_puts(buf, o, "</scan:ScanSettings>\n" as *u8)
59 return o
60}
61
62// First index of needle (length nlen) in buf[0..n), or -1. Bounded.
63func nx_escl_find(buf: *u8, n: i64, needle: *u8, nlen: i64) -> i64 {
64 if nlen <= 0 { return 0 - 1 }
65 if nlen > n { return 0 - 1 }
66 let last: i64 = n - nlen
67 var i: i64 = 0
68 var keep: i64 = 1
69 var found: i64 = 0 - 1
70 while keep == 1 {
71 if i > last { keep = 0 }
72 else {
73 var j: i64 = 0
74 var ok: i64 = 1
75 while j < nlen { if (buf[i + j] as i64) != (needle[j] as i64) { ok = 0; j = nlen } else { j = j + 1 } }
76 if ok == 1 { found = i; keep = 0 }
77 else { i = i + 1 }
78 }
79 }
80 return found
81}
82
83// 1 if buf contains needle, else 0.
84func nx_escl_has(buf: *u8, n: i64, needle: *u8, nlen: i64) -> i64 {
85 if nx_escl_find(buf, n, needle, nlen) >= 0 { return 1 }
86 return 0
87}
88
89// Integer value immediately following an opening tag like "<scan:MaxWidth>" (tagopen incl. the '>').
90// Returns the parsed integer, or -1 if the tag is absent / not followed by digits.
91func nx_escl_caps_int(buf: *u8, n: i64, tagopen: *u8, tlen: i64) -> i64 {
92 let idx: i64 = nx_escl_find(buf, n, tagopen, tlen)
93 if idx < 0 { return 0 - 1 }
94 var o: i64 = idx + tlen
95 var v: i64 = 0
96 var any: i64 = 0
97 var keep: i64 = 1
98 while keep == 1 {
99 if o >= n { keep = 0 }
100 else {
101 let ch: i64 = buf[o] as i64
102 if ch >= 48 { if ch <= 57 { v = v * 10 + (ch - 48); any = 1; o = o + 1 } else { keep = 0 } }
103 else { keep = 0 }
104 }
105 }
106 if any == 0 { return 0 - 1 }
107 return v
108}