@@ -123,6 +123,9 @@ containing optional keys that affect the behaviour of BWIPP:
123123- ` enabledontdraw ` - Allow the user to set dontdraw, in case they are providing custom renderers
124124- ` hooks ` - Dictionary of hook procedures for debugging purposes, including profiling
125125- ` default_* ` - Defaults for certain renderer options set by the user, e.g. ` default_barcolor `
126+ - ` loosespec ` - Enable best-fit specification mode globally (implies ` strictspec ` , lenient)
127+ - ` strictspec ` - Enable strict physical specification mode globally
128+ - ` propspec ` - Enable proportional specification mode globally (linear only)
126129
127130For example:
128131
@@ -360,7 +363,7 @@ Example for an encoder:
360363 % 5. Apply AST spec overrides and resolve physical specification
361364 %
362365 /encoder ast /apply_ast //render exec not { //raiseerror exec } if
363- /resolve_physspec //render exec
366+ /resolve_strictspec //render exec
364367
365368 %
366369 % 6. Main encoding logic using //encoder.staticdata and loaded data from latevars
@@ -388,7 +391,7 @@ Example for an encoder:
388391 /ren /renlinear % or /renmatrix, /renmaximatrix
389392 % /sbs [...] % 1D: space-bar-space widths
390393 % /pixs [...] % 2D: row-major pixel array
391- /physspec physspec
394+ /strictspec strictspec
392395 /xdim xdim
393396 /xmin xmin
394397 /xmax xmax
@@ -587,7 +590,7 @@ pixel-locking at 72 DPI:
587590 on a square grid.
588591
589592External scaling or the ` width ` /` height ` options resize from these defaults.
590- The ` physspec ` system (below) provides an alternative path that renders at
593+ The ` strictspec ` system (below) provides an alternative path that renders at
591594specification-accurate physical dimensions.
592595
593596
@@ -605,7 +608,7 @@ cause gridfit to silently skip via the `hwxres` guard.
605608** Rounding modes:**
606609- Default: round to nearest whole pixel per module
607610- ` width ` forced (renlinear only): floor, to avoid exceeding requested width
608- - ` physspec ` with spec bounds: smart rounding — try round first; if the
611+ - ` strictspec ` with spec bounds: smart rounding — try round first; if the
609612 effective X-dimension falls outside ` xmin ` /` xmax ` , try the alternate
610613 direction; if neither fits, return ` /errorname (info) false ` to the caller
611614
@@ -616,70 +619,163 @@ error via `raiseerror`.
616619
617620** EPS safety:** When ` gridfit ` is not enabled (and ` griddpi ` is not set), no
618621device-dependent operators (` defaultmatrix ` , ` dtransform ` ) are executed.
619- ` physspec ` without gridfit is fully EPS-safe.
622+ ` strictspec ` without gridfit is fully EPS-safe.
620623
621624
622625### Physical Specification System (` render.ps.src ` , encoders)
623626
624627Shared helpers in the ` render ` resource enable encoders to generate output at
625628physical specification dimensions.
626629
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+ ** Specification modes** control how symbology dimensions relate to spec:
631+
632+ - ** No spec** (default): 1pt per module. Encoder's default bar height.
633+ Modules land on pixel boundaries at 72 DPI. The user scales externally
634+ to reach their target X-dimension.
635+
636+ - ** ` propspec ` ** (linear only): 1pt per module, but bar height is derived
637+ from the spec ratio ` hnom/xnom ` , rounded to a whole number of points
638+ (pixel-locked). The coordinate system stays at 1pt-per-module so modules
639+ remain pixel-locked at 72 DPI — the user applies a single scale factor
640+ to hit both their target X-dimension and resolution. Silently falls back
641+ to default height when ` hnom ` is not available (harmless no-op).
642+ User-supplied ` height ` overrides the derived value. EPS-safe.
643+
644+ - ** ` strictspec ` ** : The renderer scales the symbol to physical spec
645+ dimensions derived from ` xnom ` (or explicit ` xdim ` ). Bar height is
646+ derived from ` hnom/xnom ` (not pixel-locked). The output is at the
647+ correct absolute size for direct placement in a page layout. Use with
648+ ` gridfit ` /` griddpi ` to additionally snap to device pixels for bitmap
649+ output. EPS-safe without gridfit. ` gridfit ` without explicit ` griddpi `
650+ probes the device via ` defaultmatrix ` /` dtransform ` (not EPS-safe).
651+ Strict: errors if ` xnom ` /` xdim ` missing or effective X outside bounds.
652+
653+ - ** ` loosespec ` ** : Implies ` strictspec ` . Lenient variant that silently falls
654+ back to default dimensions when ` xnom ` /` xdim ` is missing, suppresses
655+ bounds violations, and picks the closest-to-spec gridfit snap when
656+ neither rounding direction is in-spec. The recommended mode for
657+ general-purpose output.
658+
659+ ` propspec ` exists because ` strictspec ` changes the coordinate system away
660+ from 1pt-per-module, which makes external scaling for bitmap generation
661+ less predictable. ` propspec ` retains the 1pt base while applying the spec
662+ height ratio — the only spec-aware mode that is fully EPS-safe and
663+ naturally pixel-locked.
664+
665+ All three modes (` strictspec ` , ` propspec ` , ` loosespec ` ) can be set per-symbol
666+ via options or globally via ` global_ctx ` . Each encoder reads global
667+ defaults via the ` global_encoder_defaults ` dispatch helper, guarded by
668+ ` _dontdraw ` so that sub-encoders called by wrappers do not re-read
669+ global defaults. Typical global configurations:
630670
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
671+ ``` postscript
672+ /uk.co.terryburton.bwipp.global_ctx << /loosespec true >> def % General purpose (vector, EPS)
673+ /uk.co.terryburton.bwipp.global_ctx << /loosespec true /gridfit true >> def % Bitmap output
674+ /uk.co.terryburton.bwipp.global_ctx << /strictspec true >> def % Strict validation
675+ ```
642676
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).
677+ ** Options:** ` strictspec ` (bool), ` propspec ` (bool, linear only), ` loosespec `
678+ (bool), ` mag ` (real, default 1.0), ` xdim ` (mm), ` hdim ` (mm), ` ast `
679+ (string, default ` (default) ` ), ` xnom ` /` hnom ` /` xmin ` /` xmax ` (mm, sentinel
680+ -1.0), ` modunit ` (int). ` height ` defaults to -1.0 (sentinel); each
681+ encoder falls back to its own default when the sentinel survives.
646682
647683** Application Specification Tables (ASTs):** ` render.ast ` is a static readonly
648684dict mapping encoder names to named spec profiles (GS1 SSTs 1-13, ISO
649685defaults). Entries may share dicts via ` index ` to reduce allocation.
650686` apply_ast ` fills -1.0 sentinel values from the selected profile into the
651687caller'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.
688+ preserved. When the encoder is in the AST table but the requested profile
689+ is not found, ` default ` silently passes; non-default profiles error.
655690
656691** Helpers in render (dispatch table):**
657692
693+ - ` global_encoder_defaults ` — reads ` loosespec ` , ` strictspec ` , ` propspec ` from
694+ ` global_ctx ` when local values are false; guarded by ` _dontdraw ` to
695+ prevent sub-encoders from re-reading. ` loosespec ` implies ` strictspec ` .
696+ - ` global_renderer_defaults ` — reads ` default_barcolor ` ,
697+ ` default_backgroundcolor ` , ` default_bordercolor ` , ` default_inkspread ` ,
698+ ` default_griddpi ` , ` default_gridfit ` from ` global_ctx `
658699- ` apply_ast ` — fills spec sentinels from AST; returns ` true ` or
659700 ` /errorname (info) false `
660- - ` resolve_physspec ` — validates ` mag ` /` xdim ` exclusivity, resolves ` xdim `
701+ - ` resolve_strictspec ` — validates ` mag ` /` xdim ` exclusivity, resolves ` xdim `
661702 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
703+ caller's dict), validates bounds via ` validate_xdim ` . Under ` loosespec ` :
704+ silently disables ` strictspec ` when ` xnom ` /` xdim ` missing, suppresses
705+ bounds errors
706+ - ` resolve_height ` — pure function, returns derived height on the stack
707+ (or current ` height ` if not applicable). Derives when
708+ ` hnom != -1 AND (strictspec OR (propspec AND height == sentinel)) ` .
709+ Pixel-locks (rounds) under propspec; not under strictspec.
666710- ` validate_xdim ` — low-level ` xdim xmin xmax ` bounds check; returns ` true `
667711 or ` /errorname (info) false ` with formatted error string
668712
713+ ** Encoder integration pattern** (see ` ean13.ps.src ` for linear,
714+ ` qrcode.ps.src ` for matrix):
715+
716+ 1 . Declare spec options with -1.0 sentinels (including ` height ` ,
717+ ` loosespec ` , ` propspec ` , ` strictspec ` )
718+ 2 . After processoptions, apply global defaults:
719+ ``` postscript
720+ /apply //processoptions exec /options exch def
721+ /global_encoder_defaults //render exec
722+ ```
723+ 3 . After input validation, call AST and strictspec resolution:
724+ ``` postscript
725+ /ENCODER ast /apply_ast //render exec not { //raiseerror exec } if
726+ /resolve_strictspec //render exec
727+ ```
728+ 4 . For linear encoders, resolve height with fallback to encoder default:
729+ ``` postscript
730+ /height /resolve_height //render exec dup -1 eq { pop <default> } if def
731+ ```
732+ 5 . Pass ` strictspec ` , ` loosespec ` , ` xdim ` , ` xmin ` , ` xmax ` , ` modunit ` in the
733+ intermediate dict
734+
735+ ** Linear encoders** add ` propspec ` , ` hdim ` , ` hnom ` and call ` resolve_height ` .
736+ ** Matrix encoders** omit these; ` modunit ` is typically 2 (1 for stacked-linear
737+ types such as pdf417, micropdf417, codablockf, code16k, code49).
738+
669739** Renderer scaling:** Each renderer applies `xdim 72 mul 25.4 div modunit div
670- dup scale` when ` physspec ` is true. ` renmaximatrix` divides additionally by
740+ dup scale` when ` strictspec ` is true. ` renmaximatrix` divides additionally by
671741its existing 2.4945 scale factor.
672742
673- ** Inkspread under physspec :** Renderers adjust inkspread to maintain a fixed
743+ ** Inkspread under strictspec :** Renderers adjust inkspread to maintain a fixed
674744physical amount (mm) rather than a proportional reduction. For ` renlinear ` ,
675745the adjustment is applied before bar width computation (since bars are
676746pre-computed into the ` bars ` array). For ` renmatrix ` and ` renmaximatrix ` , it
677- is applied after the physspec scale, before module rendering.
747+ is applied after the strictspec scale, before module rendering.
748+
749+ ** Wrapper framework:** The outermost encoder consumes the AST. All wrappers
750+ that have their own AST entry must:
751+
752+ 1 . Declare spec options (including ` propspec ` , ` hnom ` , ` ast ` )
753+ 2 . Call ` apply_ast ` under their own encoder name
754+ 3 . Call ` resolve_strictspec `
755+ 4 . ` options (ast) undef ` and ` options (mag) undef ` — consumed, do not
756+ leak to inner encoder
757+ 5 . Put all resolved spec values into ` options ` for the inner encoder
758+ 6 . For wrappers with a non-1.0 height default: fall back only when the
759+ sentinel survives and propspec won't derive:
760+ ``` postscript
761+ height -1.0 eq { propspec hnom -1 ne and not { /height <default> def } if } if
762+ ```
678763
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 ` ).
764+ Simple wrappers without their own AST (code32, hibccode128, etc.) need no
765+ spec handling — options flow through transparently. Their ` height ` should
766+ default to -1.0 (sentinel) so propspec/strictspec can derive via the inner
767+ encoder.
768+
769+ ** Composite wrappers** call both a linear encoder and gs1-cc. Spec options
770+ must reach the linear encoder but NOT gs1-cc (the 2D component has
771+ independent scaling). Strip ` strictspec ` /` propspec ` from options after the
772+ linear encoder call and before the gs1-cc call:
773+ ``` postscript
774+ options (strictspec) undef
775+ options (propspec) undef
776+ ```
777+ For ` gs1-128composite ` which uses ` << options {} forall >> ` clones, strip
778+ from the clone passed to gs1-cc.
683779
684780
685781## Performance
0 commit comments