@@ -357,7 +357,13 @@ Example for an encoder:
357357 } if
358358
359359 %
360- % 5. Main encoding logic using //encoder.staticdata and loaded data from latevars
360+ % 5. Apply AST spec overrides and resolve physical specification
361+ %
362+ /encoder ast /apply_ast //render exec not { //raiseerror exec } if
363+ /resolve_physspec //render exec
364+
365+ %
366+ % 6. Main encoding logic using //encoder.staticdata and loaded data from latevars
361367 %
362368 % The barcode generation process is typically some variation of these steps:
363369 % - High-level encoding to codewords
@@ -371,17 +377,27 @@ Example for an encoder:
371377 %
372378
373379 %
374- % 6. Build output dictionary
380+ % 7. Resolve bar height from spec (linear only, before bhs/bbs construction)
381+ %
382+ /resolve_height //render exec
383+
384+ %
385+ % 8. Build output dictionary
375386 %
376387 <<
377388 /ren /renlinear % or /renmatrix, /renmaximatrix
378389 % /sbs [...] % 1D: space-bar-space widths
379390 % /pixs [...] % 2D: row-major pixel array
391+ /physspec physspec
392+ /xdim xdim
393+ /xmin xmin
394+ /xmax xmax
395+ /modunit modunit
380396 /opt options
381397 >>
382398
383399 %
384- % 7 . Conditional rendering
400+ % 9 . Conditional rendering
385401 %
386402 dontdraw not //renlinear if
387403
@@ -556,6 +572,116 @@ Encoders create a common dictionary structure expected by their renderer:
556572Callers can access the intermediate dictionary by setting the ` dontdraw ` options with ` enabledontdraw ` set in global context.
557573
558574
575+ ### Default Normalised Units
576+
577+ The renderers operate in harmonised normalised units chosen for integer
578+ pixel-locking at 72 DPI:
579+
580+ - ** Linear (` renlinear ` ):** 1pt per X-dimension module (narrow bar), 72pt
581+ (1 inch) default bar height. At 72 DPI, 1 module = 1 device pixel.
582+ - ** Matrix (` renmatrix ` ):** ` modunit ` points per module (1pt or 2pt depending
583+ on symbology). Output dict ` width ` /` height ` = ` cols * modunit / 72 ` inches.
584+ - ** MaxiCode (` renmaximatrix ` ):** Fixed 2.4945 scale from 1pt modules to
585+ 0.88mm hexagons. Hex geometry uses √3-based constants for correct aspect
586+ ratio at any scale; pixel-locking of hex vertices is inherently imperfect
587+ on a square grid.
588+
589+ External scaling or the ` width ` /` height ` options resize from these defaults.
590+ The ` physspec ` system (below) provides an alternative path that renders at
591+ specification-accurate physical dimensions.
592+
593+
594+ ### Grid Fitting (` render.ps.src ` )
595+
596+ ` render.gridfit ` snaps module boundaries to whole device pixels, eliminating
597+ anti-aliasing artefacts in bitmap output.
598+
599+ ** Device resolution:** When ` griddpi ` is provided by the caller, it is used
600+ directly as the target resolution — no hardware probing occurs and gridfit
601+ can operate on devices that would otherwise be skipped (e.g., nullpage).
602+ When auto-detected (` griddpi=0 ` ), ` defaultmatrix ` is queried; bogus values
603+ cause gridfit to silently skip via the ` hwxres ` guard.
604+
605+ ** Rounding modes:**
606+ - Default: round to nearest whole pixel per module
607+ - ` width ` forced (renlinear only): floor, to avoid exceeding requested width
608+ - ` physspec ` with spec bounds: smart rounding — try round first; if the
609+ effective X-dimension falls outside ` xmin ` /` xmax ` , try the alternate
610+ direction; if neither fits, return ` /errorname (info) false ` to the caller
611+
612+ ** Signature:** ` magnitude xdim xmin xmax usefloor snapy /gridfit //render exec `
613+ returns ` true ` on success, ` /errorname (info) false ` on spec violation. The
614+ caller (renderer) is responsible for cleanup (` grestore ` ) before raising the
615+ error via ` raiseerror ` .
616+
617+ ** EPS safety:** When ` gridfit ` is not enabled (and ` griddpi ` is not set), no
618+ device-dependent operators (` defaultmatrix ` , ` dtransform ` ) are executed.
619+ ` physspec ` without gridfit is fully EPS-safe.
620+
621+
622+ ### Physical Specification System (` render.ps.src ` , encoders)
623+
624+ Shared helpers in the ` render ` resource enable encoders to generate output at
625+ physical specification dimensions.
626+
627+ ** Options:** ` physspec ` (bool), ` propspec ` (bool, linear only), ` mag ` (real,
628+ default 1.0), ` xdim ` (mm), ` hdim ` (mm), ` ast ` (string, default ` (default) ` ),
629+ ` xnom ` /` hnom ` /` xmin ` /` xmax ` (mm, sentinel -1.0), ` modunit ` (int).
630+
631+ ** Encoder integration pattern** (see ` ean13.ps.src ` for linear,
632+ ` qrcode.ps.src ` for matrix):
633+
634+ 1 . Declare spec options with -1.0 sentinels after encoder-specific options
635+ 2 . After input validation, call AST and physspec resolution:
636+ ``` postscript
637+ /ENCODER ast /apply_ast //render exec not { //raiseerror exec } if
638+ /resolve_physspec //render exec
639+ ```
640+ 3 . For linear encoders, call ` /resolve_height //render exec ` before bhs/bbs
641+ 4 . Pass ` physspec ` , ` xdim ` , ` xmin ` , ` xmax ` , ` modunit ` in the intermediate dict
642+
643+ ** Linear encoders** add ` propspec ` , ` hdim ` , ` hnom ` and call ` resolve_height ` .
644+ ** Matrix encoders** omit these; ` modunit ` is typically 2 (1 for stacked-linear
645+ types such as pdf417, micropdf417, codablockf, code16k, code49).
646+
647+ ** Application Specification Tables (ASTs):** ` render.ast ` is a static readonly
648+ dict mapping encoder names to named spec profiles (GS1 SSTs 1-13, ISO
649+ defaults). Entries may share dicts via ` index ` to reduce allocation.
650+ ` apply_ast ` fills -1.0 sentinel values from the selected profile into the
651+ caller's dict using ` currentdict ` ; user-provided values (non-sentinel) are
652+ preserved. GS1 wrappers must put their ` ast ` value into ` options ` so the
653+ inner encoder's ` apply_ast ` uses the wrapper's AST context rather than its
654+ own default.
655+
656+ ** Helpers in render (dispatch table):**
657+
658+ - ` apply_ast ` — fills spec sentinels from AST; returns ` true ` or
659+ ` /errorname (info) false `
660+ - ` resolve_physspec ` — validates ` mag ` /` xdim ` exclusivity, resolves ` xdim `
661+ from ` xnom * mag ` , resolves ` hdim ` from ` hnom * mag ` (if defined in
662+ caller's dict), validates bounds via ` validate_xdim `
663+ - ` resolve_height ` — sets linear ` height ` from ` hnom/xnom * modunit / 72 `
664+ ratio; only runs when ` propspec ` or ` physspec ` is active and ` hnom ` is
665+ defined
666+ - ` validate_xdim ` — low-level ` xdim xmin xmax ` bounds check; returns ` true `
667+ or ` /errorname (info) false ` with formatted error string
668+
669+ ** Renderer scaling:** Each renderer applies `xdim 72 mul 25.4 div modunit div
670+ dup scale` when ` physspec` is true. ` renmaximatrix` divides additionally by
671+ its existing 2.4945 scale factor.
672+
673+ ** Inkspread under physspec:** Renderers adjust inkspread to maintain a fixed
674+ physical amount (mm) rather than a proportional reduction. For ` renlinear ` ,
675+ the adjustment is applied before bar width computation (since bars are
676+ pre-computed into the ` bars ` array). For ` renmatrix ` and ` renmaximatrix ` , it
677+ is applied after the physspec scale, before module rendering.
678+
679+ ** Wrappers:** Simple wrappers (isbn, code32, etc.) need no spec changes —
680+ options flow through via the ` options ` dict to the inner encoder. GS1 wrappers
681+ that tighten bounds must declare spec options and explicitly put them into
682+ ` options ` before calling the inner encoder (see ` gs1qrcode.ps.src ` ).
683+
684+
559685## Performance
560686
561687### Ghostscript Observed Behaviour
0 commit comments