Skip to content

Commit e01607d

Browse files
committed
dotcode: Score each mask and its lit-edge variant in a single pass
The lit-edge variant of a masked symbol differs only at the six corner-adjacent edge dots, so evalsymbol derives its score from the same traversal: each side scan's tally is extended by the dots on that side, the blank-group scans keep a second accumulator that breaks at the columns and rows containing a dot, and the void count is adjusted by rescoring just the positions whose neighbourhood contains a dot.
1 parent 315f4db commit e01607d

3 files changed

Lines changed: 169 additions & 42 deletions

File tree

CHANGES

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
XXXX-XX-XX
1+
2026-07-05
22

3-
* The performance of the DotCode encoder was improved by sharing a single codeword placement walk between the mask candidates and by deriving each candidate's error correction codewords from those of the unmasked message.
3+
* The performance of the DotCode encoder was improved by sharing a single codeword placement walk between the mask candidates, by deriving each candidate's error correction codewords from those of the unmasked message, and by scoring each candidate together with its lit-edge variant in a single evaluation pass.
44
* Profiling hooks were added for the QR Code mask application and evaluation phases and for the renmatrix region tracing sub-phases.
55

66

CONTRIBUTING.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -806,6 +806,7 @@ from the clone passed to gs1-cc.
806806
- Skip a transform whose result is a known no-op rather than computing it (the identity mask yields an all-zero layer, so evaluate the unmasked symbol directly; see `hanxin`).
807807
- Classify positions in a hot scan by indexing a static string with a small per-position code, spilling to variables only on the rare matching branch (stem-corner detection in the region trace; see `renmatrix`).
808808
- Hoist a traversal whose result is candidate-invariant out of the per-candidate loop (the codeword placement walk visits vacant positions in a mask-independent order, so resolve the position sequence once and share it between masks; see `dotcode`).
809+
- Score a symbol together with a variant that differs at a few known positions by keeping a second set of tallies in the same traversal, adjusted only where the variant differs (the lit-edge variant in mask evaluation; see `dotcode`).
809810

810811
**Conditional Assignment Pattern**
811812

@@ -1137,7 +1138,12 @@ create a hooks entries named like `qrcode.before`.
11371138

11381139
### Profiling Results
11391140

1140-
Performance enhancements are welcome, but significant gains are hard won. Large 2D symbols have different bottlenecks: QR Code, Han Xin and DotCode are mask evaluation bound (DotCode's cost roughly doubles when the score threshold fails and the lit-edge variants are re-scored); Data Matrix is RSEC bound (mitigated by FIFO caches). Aztec splits roughly evenly between input encoding and RSEC. PDF417 and MicroPDF417 are fast enough that no single phase dominates. See encoder source for attempted optimizations.
1141+
Performance enhancements are welcome, but significant gains are hard won.
1142+
Large 2D symbols have different bottlenecks: QR Code, Han Xin and DotCode are
1143+
mask evaluation bound; Data Matrix is RSEC bound (mitigated by FIFO caches).
1144+
Aztec splits roughly evenly between input encoding and RSEC. PDF417 and
1145+
MicroPDF417 are fast enough that no single phase dominates. See encoder
1146+
source for attempted optimizations.
11411147

11421148

11431149
## Testing

src/dotcode.ps.src

Lines changed: 160 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1057,6 +1057,52 @@ begin
10571057
outline exch aload pop dmv 1 put
10581058
} forall
10591059

1060+
%
1061+
% Fixed per-geometry aids for scoring each symbol together with its
1062+
% variant that has the six edge dots forcibly lit: the dots falling on
1063+
% each scanned side (two per side), the interior columns and rows
1064+
% containing a dot, and the padded void-scan positions whose
1065+
% neighbourhood contains a dot
1066+
%
1067+
/sidedots [
1068+
[ sixedges { aload pop /sy exch def /sx exch def sy 0 eq { [ sx sx sy dmv ] } if } forall ]
1069+
[ sixedges { aload pop /sy exch def /sx exch def sy rows 1 sub eq { [ sx sx sy dmv ] } if } forall ]
1070+
[ sixedges { aload pop /sy exch def /sx exch def sx 0 eq { [ sy sx sy dmv ] } if } forall ]
1071+
[ sixedges { aload pop /sy exch def /sx exch def sx columns 1 sub eq { [ sy sx sy dmv ] } if } forall ]
1072+
] def
1073+
/evalsides [
1074+
[ /x 0 sidedots 0 get ] % Top
1075+
[ /x 1 sidedots 1 get ] % Bottom
1076+
[ /y 0 sidedots 2 get ] % Left
1077+
[ /y 1 sidedots 3 get ] % Right
1078+
] def
1079+
/litpencols [ sixedges { 0 get dup 1 ge 1 index columns 2 sub le and not {pop} if } forall ] def
1080+
/litpc0 litpencols 0 get def
1081+
/litpc1 litpencols dup length 1 sub get def
1082+
/litpenrows [ sixedges { 1 get dup 1 ge 1 index rows 2 sub le and not {pop} if } forall ] def
1083+
/litpr0 litpenrows 0 get def
1084+
/litpr1 litpenrows dup length 1 sub get def
1085+
/pcols columns 4 add def
1086+
/litsetposs [ sixedges { aload pop 2 add pcols mul exch 2 add add } forall ] def
1087+
/voidoffs [ 0 2 -2 pcols dup add pcols dup add neg
1088+
pcols 1 add pcols 1 sub pcols 1 add neg pcols 1 sub neg ] def
1089+
/voidposs 54 array def
1090+
/nvoid 0 def
1091+
litsetposs {
1092+
voidoffs {
1093+
1 index add /vq exch def
1094+
vq pcols mod dup 2 ge exch pcols 3 sub le and
1095+
vq pcols idiv dup 2 ge exch rows 1 add le and
1096+
and { % Within the void scan window
1097+
/seenq false def
1098+
0 1 nvoid 1 sub { voidposs exch get vq eq { /seenq true def } if } for
1099+
seenq not { voidposs nvoid vq put /nvoid nvoid 1 add def } if
1100+
} if
1101+
} forall
1102+
pop
1103+
} forall
1104+
/voidposs voidposs 0 nvoid getinterval def
1105+
10601106
/clearcol {
10611107
/x exch def
10621108
true
@@ -1073,18 +1119,40 @@ begin
10731119
} for
10741120
} def
10751121

1122+
/voidat { % Whether a padded void-scan position counts as a void or an isolated dot
1123+
/vi exch def
1124+
1 { % Common exit
1125+
symp vi pcols sub 1 sub get 1 eq {false exit} if
1126+
symp vi pcols sub 1 add get 1 eq {false exit} if
1127+
symp vi pcols add 1 sub get 1 eq {false exit} if
1128+
symp vi pcols add 1 add get 1 eq {false exit} if
1129+
symp vi get 0 eq {true exit} if
1130+
symp vi 2 sub get 1 eq {false exit} if
1131+
symp vi 2 add get 1 eq {false exit} if
1132+
symp vi pcols dup add sub get 1 eq {false exit} if
1133+
symp vi pcols dup add add get 1 eq {false exit} if
1134+
true
1135+
} repeat
1136+
} def
1137+
10761138
%
1077-
% Evaluate the symbol
1139+
% Evaluate the symbol, returning its score together with the score of
1140+
% its variant that has the six edge dots forcibly lit. The variant
1141+
% differs only at those dots, so its score is derived from the same
1142+
% traversal by adjusting each tally where the dots contribute
10781143
%
10791144
/evalsymbol {
10801145
/sym exch def
10811146

10821147
%
1083-
% Identify the worst side
1148+
% Identify the worst side; the dots lie on the scanned sides so the
1149+
% lit tallies extend each side's count and span. Every side gains
1150+
% two dots, hence the lit variant never has an unlit edge
10841151
%
10851152
/worst 9999999 def
1086-
[ [ /x 0 ] [ /x 1 ] [ /y 0 ] [ /y 1 ] ] { % Top Bottom Left Right
1087-
aload pop /fl exch def /dir exch def
1153+
/litworst 9999999 def
1154+
evalsides { % Top Bottom Left Right
1155+
aload pop /dots exch def /fl exch def /dir exch def
10881156
/sum 0 def /first -1 def /last -1 def
10891157
0 1 dir /x eq {columns} {rows} ifelse 1 sub {
10901158
dir exch def
@@ -1098,51 +1166,94 @@ begin
10981166
/sum sum 1 add def
10991167
} if
11001168
} for
1169+
/lsum sum def /lfirst first def /llast last def
1170+
dots {
1171+
aload pop sym exch get 0 eq {
1172+
/spos exch def
1173+
/lsum lsum 1 add def
1174+
lfirst -1 eq lfirst spos gt or {/lfirst spos def} if
1175+
llast spos lt {/llast spos def} if
1176+
} {pop} ifelse
1177+
} forall
11011178
sum last add first sub
11021179
dir /x eq {rows} {columns} ifelse mul
11031180
dup worst lt {/worst exch def} {pop} ifelse
1181+
lsum llast add lfirst sub
1182+
dir /x eq {rows} {columns} ifelse mul
1183+
dup litworst lt {/litworst exch def} {pop} ifelse
11041184
} forall
11051185

11061186
/pen 0 def
1187+
/litpen 0 def
11071188

11081189
%
11091190
% Check for groups of blank columns unless data is encoded vertically
11101191
% in a symbol of sufficient height to overcome up to six consecutive 0s
1111-
% in two neighbouring 5 of 9 dot patterns
1192+
% in two neighbouring 5 of 9 dot patterns. A forcibly lit dot breaks
1193+
% the lit variant's group at its column
11121194
%
11131195
rows 2 mod 1 eq rows 12 le or {
11141196
/sum 0 def /p 0 def
1115-
1 1 columns 2 sub {
1116-
clearcol {
1197+
/lsum 0 def /lp 0 def
1198+
1 1 columns 2 sub { % cx kept on stack
1199+
dup clearcol {
11171200
/sum sum 1 add def
11181201
/p sum 1 eq {rows} {p rows mul} ifelse def
1202+
dup litpc0 eq exch litpc1 eq or {
1203+
/lsum 0 def
1204+
/litpen litpen lp add def
1205+
/lp 0 def
1206+
} {
1207+
/lsum lsum 1 add def
1208+
/lp lsum 1 eq {rows} {lp rows mul} ifelse def
1209+
} ifelse
11191210
} {
1211+
pop
11201212
/sum 0 def
11211213
/pen pen p add def
11221214
/p 0 def
1215+
/lsum 0 def
1216+
/litpen litpen lp add def
1217+
/lp 0 def
11231218
} ifelse
11241219
} for
11251220
/pen pen p add def
1221+
/litpen litpen lp add def
11261222
} if
11271223

11281224
%
11291225
% Check for groups of blank rows unless data is encoded horizontally
11301226
% in a symbol of sufficient width to overcome up to six consecutive 0s
1131-
% in two neighbouring 5 of 9 dot patterns
1227+
% in two neighbouring 5 of 9 dot patterns. A forcibly lit dot breaks
1228+
% the lit variant's group at its row
11321229
%
11331230
rows 2 mod 0 eq columns 12 le or {
11341231
/sum 0 def /p 0 def
1135-
1 1 rows 2 sub {
1136-
clearrow {
1232+
/lsum 0 def /lp 0 def
1233+
1 1 rows 2 sub { % cy kept on stack
1234+
dup clearrow {
11371235
/sum sum 1 add def
11381236
/p sum 1 eq {columns} {p columns mul} ifelse def
1237+
dup litpr0 eq exch litpr1 eq or {
1238+
/lsum 0 def
1239+
/litpen litpen lp add def
1240+
/lp 0 def
1241+
} {
1242+
/lsum lsum 1 add def
1243+
/lp lsum 1 eq {columns} {lp columns mul} ifelse def
1244+
} ifelse
11391245
} {
1246+
pop
11401247
/sum 0 def
11411248
/pen pen p add def
11421249
/p 0 def
1250+
/lsum 0 def
1251+
/litpen litpen lp add def
1252+
/lp 0 def
11431253
} ifelse
11441254
} for
11451255
/pen pen p add def
1256+
/litpen litpen lp add def
11461257
} if
11471258

11481259
%
@@ -1185,19 +1296,31 @@ begin
11851296
/columns columns 4 sub def
11861297
/rows rows 4 sub def
11871298

1299+
%
1300+
% Only positions whose neighbourhood contains an edge dot can count
1301+
% differently in the lit variant, so rescore just those positions
1302+
% before and after lighting the dots in the padded copy
1303+
%
1304+
/litsum sum def
1305+
voidposs { voidat {/litsum litsum 1 sub def} if } forall
1306+
litsetposs { symp exch 1 put } forall
1307+
voidposs { voidat {/litsum litsum 1 add def} if } forall
1308+
11881309
worst 0 eq { % Fixed penalty to disqualify a symbol with an unlit edge
11891310
-99999
11901311
} {
11911312
worst sum sum mul sub pen sub
11921313
} ifelse
1314+
litworst litsum litsum mul sub litpen sub
11931315

11941316
} def
11951317

11961318
/step nw 112 idiv 1 add def
11971319
/offset {step mul start add} def
11981320

11991321
%
1200-
% Evaluate the symbols in the following order:
1322+
% Each evaluation scores a symbol m and its lit-edge variant m' in a
1323+
% single pass. Selection order:
12011324
% fast mode: m3 -> m3' -> m2 -> m2' -> m1 -> m1' -> m0 -> m0'
12021325
% dflt mode: (m0 m1 m2 m3) -> (m0' m1' m2' m3')
12031326
% -> : Test if we have met the HxW/2 threshold
@@ -1210,7 +1333,8 @@ begin
12101333
} {
12111334
fast { [ 2#11 2#10 2#01 2#00 ] } { [ 2#00 2#01 2#10 2#11 ] } ifelse
12121335
} ifelse def
1213-
/litmasks 4 array def
1336+
/masksyms 4 array def
1337+
/litscores 4 array def
12141338

12151339
%
12161340
% The walk visits vacant positions in an order that is independent of the
@@ -1351,65 +1475,62 @@ begin
13511475
(matrix.evaluate.normal) //dotcode.before exec
13521476

13531477
%
1354-
% Evaluate the symbol to find the most suitable mask
1478+
% Evaluate the symbol and its lit-edge variant to find the most
1479+
% suitable mask
13551480
%
1356-
/score pixs evalsymbol def
1481+
pixs evalsymbol /litscore exch def /score exch def
1482+
masksyms mask pixs put
1483+
litscores mask litscore put
13571484
score bestscore gt {
1358-
/bestsym pixs def
1485+
/bestsym pixs def /bestlit false def
13591486
/bestscore score def
13601487
fast bestscore rows columns mul 2 idiv gt and {exit} if
13611488
} if
13621489

13631490
%
1364-
% Make a copy with lit edges for possible later evaluation
1365-
%
1366-
/litmask pixs dup length array copy def
1367-
0 1 5 { % Light last six edge dots
1368-
/i exch def
1369-
litmask sixedges i get aload pop dmv
1370-
1 put
1371-
} for
1372-
litmasks mask litmask put
1373-
1374-
%
1375-
% In fast mode immediately score the lit symbol
1491+
% In fast mode the lit symbol competes immediately
13761492
%
13771493
fast {
1378-
/score litmask evalsymbol def
1379-
score bestscore gt {
1380-
/bestsym litmask def
1381-
/bestscore score def
1494+
litscore bestscore gt {
1495+
/bestsym pixs def /bestlit true def
1496+
/bestscore litscore def
13821497
bestscore rows columns mul 2 idiv gt {exit} if
13831498
} if
13841499
} if
13851500

13861501
(matrix.evaluate.normal) //dotcode.after exec
13871502

13881503
} forall
1389-
/pixs bestsym def
13901504

13911505
%
1392-
% For default mode, if best mask fails threshold then recalculate with lit corners and pick best
1506+
% For default mode, if best mask fails threshold then pick the best lit variant
13931507
%
13941508
fast not bestscore rows columns mul 2 idiv le and {
13951509

13961510
(matrix.evaluate.fixup) //dotcode.before exec
13971511

13981512
/bestscore -99999999 def
13991513
masks {
1400-
litmasks exch get /litmask exch def
1401-
/score litmask evalsymbol def
1402-
score bestscore gt {
1403-
/bestsym litmask def
1404-
/bestscore score def
1514+
/mask exch def
1515+
litscores mask get bestscore gt {
1516+
/bestsym masksyms mask get def /bestlit true def
1517+
/bestscore litscores mask get def
14051518
} if
14061519
} forall
1407-
/pixs bestsym def
14081520

14091521
(matrix.evaluate.fixup) //dotcode.after exec
14101522

14111523
} if
14121524

1525+
%
1526+
% Only the winning symbol ever needs its edge dots lit, so light them
1527+
% after selection
1528+
%
1529+
/pixs bestsym def
1530+
bestlit {
1531+
sixedges { pixs exch aload pop dmv 1 put } forall
1532+
} if
1533+
14131534
<<
14141535
/ren /renmatrix
14151536
/dotty true

0 commit comments

Comments
 (0)