PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.10.13
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.10.13
1.10.19 1.10.18 1.10.17 1.10.16 1.10.15 1.10.13 1.10.14 1.10.12 1.10.11 1.10.10 1.10.9 1.10.8 untagged-3d9b7ccddc54df87c672 1.10.7 1.10.6 1.10.5 1.10.3 1.10.4 1.10.2 1.10.1 1.10.0 1.9.17 1.9.15 1.9.16 1.9.14 All 163 releases
woocommerce-pos / includes / Templates / Barcode_Symbology.php

Barcode_Symbology.php in WCPOS – Point of Sale (POS) plugin for WooCommerce 1.10.13, at includes/Templates/Barcode_Symbology.php

685 lines 23.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Barcode symbology owner — one map, five render lanes.
4 *
5 * A receipt template names its barcode symbology once (`<barcode type="ean13">`),
6 * but that name has to be translated for every render lane we ship: raw ESC/POS
7 * bytes, raw StarPRNT bytes, Star Document Markup, ePOS-Print XML, and the
8 * picqer rasterizer used by the HTML/PDF path. Each lane spells the same nine
9 * symbologies differently, and two of them disagree about the *order* of the UPC
10 * pair, so keeping the maps next to the emitters that use them guarantees they
11 * drift. They live here instead, with a matrix test pinning every cell.
12 *
13 * Vendor references:
14 * - ESC/POS: Epson ESC/POS Command Reference, `GS k` (barcode, function B).
15 * - StarPRNT: StarPRNT Command Specifications Ver 1.3E, section 10 (barcode).
16 * - ePOS-Print XML: Epson ePOS-Print XML reference, `<barcode>` element.
17 *
18 * @author Paul Kilmurray <paul@kilbot.com>
19 *
20 * @see http://wcpos.com
21 * @package WCPOS\WooCommercePOS\Templates
22 */
23
24 namespace WCPOS\WooCommercePOS\Templates;
25
26 use WCPOS\Vendor\Picqer\Barcode\BarcodeGeneratorPNG;
27
28 /**
29 * Barcode_Symbology class.
30 *
31 * All-static and stateless: a pure translation table.
32 *
33 * Shape, and when to change it. The per-lane accessors come in pairs named for
34 * the vendor (escpos_*, starprnt_*) rather than hanging off a lane-descriptor
35 * object, because the lanes are not uniform: two want an integer selector, two
36 * want a string name, and one wants a picqer constant. A descriptor would make
37 * that return type a union and hand the XML and markup lanes members they never
38 * use. Revisit when either becomes true: a third vendor needs its own payload
39 * encoder, or the matrix test's provider grows past roughly seven columns.
40 *
41 * Known duplication this class does NOT address: emit_centered_text() is
42 * identical in Escpos_Thermal_Emitter and Starprnt_Thermal_Emitter, alongside
43 * eleven text-metric helpers those two files already duplicated before this
44 * class existed. Extracting one of the twelve into a trait would fragment the
45 * set rather than fix it; they belong together in a shared text-layout module,
46 * which is scoped separately. Do not treat this class as the pattern for that
47 * work — a static lookup owner suits a translation table keyed by lane, not
48 * shared behaviour that needs emitter state.
49 */
50 class Barcode_Symbology {
51 /**
52 * The 1D symbologies WCPOS supports, in canonical (parser) spelling.
53 *
54 * Every lane accessor below is total over this list.
55 *
56 * @var array
57 */
58 public const SYMBOLOGIES = array(
59 'code128',
60 'code39',
61 'code93',
62 'ean13',
63 'ean8',
64 'upca',
65 'upce',
66 'codabar',
67 'itf',
68 );
69
70 /**
71 * Canonical name for the 2D QR symbology, which is not a 1D barcode.
72 *
73 * @var string
74 */
75 public const QRCODE = 'qrcode';
76
77 /**
78 * Symbology used when a template names one we do not support.
79 *
80 * Code 128 encodes any printable ASCII, so it is the only symbology that can
81 * carry an arbitrary value without a data constraint of its own.
82 *
83 * @var string
84 */
85 public const DEFAULT_SYMBOLOGY = 'code128';
86
87 /**
88 * Lane discriminator for the ESC/POS byte emitter.
89 *
90 * @var string
91 */
92 public const LANE_ESCPOS = 'escpos';
93
94 /**
95 * Lane discriminator for the StarPRNT byte emitter.
96 *
97 * @var string
98 */
99 public const LANE_STARPRNT = 'starprnt';
100
101 /**
102 * Maximum barcode data bytes either byte lane will transmit.
103 *
104 * ESC/POS function B carries the length in a single byte (`GS k m n`), and
105 * the StarPRNT barcode data field is capped at the same 255 for every
106 * symbology, so one clamp covers both.
107 *
108 * @var int
109 */
110 public const MAX_DATA_BYTES = 255;
111
112 /**
113 * ESC/POS `GS k` function-B symbology selector (the `m` parameter).
114 *
115 * @var array
116 */
117 private const ESCPOS_IDS = array(
118 'upca' => 65,
119 'upce' => 66,
120 'ean13' => 67,
121 'ean8' => 68,
122 'code39' => 69,
123 'itf' => 70,
124 'codabar' => 71,
125 'code93' => 72,
126 'code128' => 73,
127 );
128
129 /**
130 * StarPRNT `ESC b n1` symbology selector.
131 *
132 * NOTE: the UPC pair is the inverse of the ESC/POS table above — Star numbers
133 * UPC-E before UPC-A. Do not transcribe one table from the other.
134 *
135 * @var array
136 */
137 private const STARPRNT_IDS = array(
138 'upce' => 0,
139 'upca' => 1,
140 'ean8' => 2,
141 'ean13' => 3,
142 'code39' => 4,
143 'itf' => 5,
144 'code128' => 6,
145 'code93' => 7,
146 'codabar' => 8,
147 );
148
149 /**
150 * Attribute values for the ePOS-Print XML `<barcode type>` element.
151 *
152 * Only the UPC pair differs from the canonical spelling: ePOS-Print
153 * underscores them, and rejects the unseparated form outright.
154 *
155 * @var array
156 */
157 private const EPOS_XML_NAMES = array(
158 'upca' => 'upc_a',
159 'upce' => 'upc_e',
160 );
161
162 /**
163 * Symbologies ePOS-Print accepts that WCPOS does not model itself.
164 *
165 * Source: Epson ePOS-Print XML reference, `<barcode>` element. These are
166 * accepted by the printer but absent from self::SYMBOLOGIES, so they are
167 * passed through untranslated instead of being folded to Code 128.
168 *
169 * @var array
170 */
171 private const EPOS_XML_ONLY_TYPES = array(
172 'jan13',
173 'jan8',
174 'code128_auto',
175 'gs1_128',
176 'gs1_databar_omnidirectional',
177 'gs1_databar_truncated',
178 'gs1_databar_limited',
179 'gs1_databar_expanded',
180 );
181
182 /**
183 * Star Document Markup `[barcode: type ...]` names.
184 *
185 * Star markup is alone in calling Codabar "NW-7".
186 *
187 * @var array
188 */
189 private const STAR_MARKUP_NAMES = array(
190 'codabar' => 'nw7',
191 );
192
193 /**
194 * ESC/POS Code 128 code-set selector prefixed to the data.
195 *
196 * Function B requires the data to open with a code-set selector; without one
197 * the printer prints nothing and reports no error. Set B carries the full
198 * printable ASCII range, which is what receipt values use.
199 *
200 * @var string
201 */
202 private const ESCPOS_CODE128_SELECTOR = '{B';
203
204 /**
205 * Reduce a template's barcode type to a canonical name.
206 *
207 * @param string $type The barcode type attribute value.
208 *
209 * @return string A member of self::SYMBOLOGIES, or self::QRCODE.
210 */
211 public static function normalize( string $type ): string {
212 $normalized = strtolower( trim( $type ) );
213
214 if ( 'qr' === $normalized ) {
215 $normalized = self::QRCODE;
216 }
217
218 // Star markup's spelling of Codabar leaks back in through templates
219 // written against a Star printer; accept it as an alias.
220 if ( 'nw7' === $normalized ) {
221 $normalized = 'codabar';
222 }
223
224 if ( self::QRCODE === $normalized ) {
225 return self::QRCODE;
226 }
227
228 return \in_array( $normalized, self::SYMBOLOGIES, true ) ? $normalized : self::DEFAULT_SYMBOLOGY;
229 }
230
231 /**
232 * Determine whether a barcode type should be rendered as a QR code.
233 *
234 * @param string $type The barcode type attribute value.
235 *
236 * @return bool True when the type is a QR variant.
237 */
238 public static function is_qr( string $type ): bool {
239 return self::QRCODE === self::normalize( $type );
240 }
241
242 /**
243 * Determine whether a value satisfies a symbology's data constraints.
244 *
245 * A printer given data it cannot encode prints nothing and reports no error,
246 * so callers on the byte lanes check this first and print the value as text
247 * instead of handing the printer a symbol it will silently drop. The same
248 * rule governs the constraints below: where a value would print as a symbol
249 * that scans back as something other than the value asked for, it is
250 * rejected here, because a silently wrong barcode is worse than no barcode.
251 *
252 * The lane is required, not optional. Two symbologies differ between lanes:
253 * UPC-E (ESC/POS accepts the 6-8 digit short form, StarPRNT requires the
254 * full 11-12 digit payload) and Code 128 (the lanes escape and select code
255 * sets differently, so both the alphabet and the encoded length differ).
256 * Defaulting either to one vendor would dress that printer's rules up as a
257 * neutral answer.
258 *
259 * @param string $type The barcode type attribute value.
260 * @param string $value The barcode value.
261 * @param string $lane Lane discriminator: self::LANE_ESCPOS or self::LANE_STARPRNT.
262 *
263 * @return bool True when the value can be encoded.
264 */
265 public static function is_valid_value( string $type, string $value, string $lane ): bool {
266 $symbology = self::normalize_linear( $type );
267 $length = \strlen( $value );
268
269 switch ( $symbology ) {
270 case 'upca':
271 if ( ! self::is_digits( $value ) ) {
272 return false;
273 }
274 // 11 digits: the printer computes the check digit. 12: the last
275 // digit is the check digit and must already be right.
276 return 11 === $length || ( 12 === $length && self::has_valid_gtin_check_digit( $value ) );
277 case 'upce':
278 // ESC/POS accepts the 6-8 digit short form as well as the full
279 // 11-12 digit form; StarPRNT accepts only the full form. The full
280 // form is a UPC-A payload the printer compresses, so its check
281 // digit is the UPC-A one. The short form's check digit is derived
282 // from an expansion only the printer performs, so it is left to
283 // the printer rather than half-checked here.
284 if ( ! self::is_digits( $value ) ) {
285 return false;
286 }
287 if ( 12 === $length ) {
288 return self::has_valid_gtin_check_digit( $value );
289 }
290 if ( 11 === $length ) {
291 return true;
292 }
293
294 return self::LANE_ESCPOS === $lane && $length >= 6 && $length <= 8;
295 case 'ean13':
296 if ( ! self::is_digits( $value ) ) {
297 return false;
298 }
299
300 return 12 === $length || ( 13 === $length && self::has_valid_gtin_check_digit( $value ) );
301 case 'ean8':
302 if ( ! self::is_digits( $value ) ) {
303 return false;
304 }
305
306 return 7 === $length || ( 8 === $length && self::has_valid_gtin_check_digit( $value ) );
307 case 'code39':
308 return self::is_valid_code39( $value );
309 case 'itf':
310 // Interleaved 2 of 5 encodes digits in pairs, so an odd-length
311 // value cannot be encoded at all.
312 return self::is_digits( $value ) && $length >= 2 && $length <= 254 && 0 === $length % 2;
313 case 'codabar':
314 // The start and stop characters travel in the data itself.
315 return $length >= 2 && $length <= 255 && 1 === preg_match( '/\A[A-Da-d][0-9\$\+\-\.\/:]*[A-Da-d]\z/', $value );
316 case 'code93':
317 return $length >= 1 && $length <= 255 && self::is_ascii( $value )
318 && ( self::LANE_STARPRNT !== $lane || 0 === preg_match( '/[\x00-\x1f\x7f]/', $value ) );
319 case 'code128':
320 default:
321 return self::is_valid_code128( $value, $lane );
322 }
323 }
324
325 /**
326 * Map a symbology to its ESC/POS `GS k` function-B selector.
327 *
328 * @param string $type The barcode type attribute value.
329 *
330 * @return int The `m` parameter byte.
331 */
332 public static function escpos_id( string $type ): int {
333 return self::ESCPOS_IDS[ self::normalize_linear( $type ) ] ?? self::ESCPOS_IDS[ self::DEFAULT_SYMBOLOGY ];
334 }
335
336 /**
337 * Build the ESC/POS data bytes for a barcode value.
338 *
339 * Code 128 alone needs a code-set selector prefixed to the data, and needs a
340 * literal `{` doubled so it is not read as one. The prefix counts toward the
341 * transmitted length, so the clamp is applied after it is added.
342 *
343 * @param string $type The barcode type attribute value.
344 * @param string $value The barcode value.
345 *
346 * @return string The data bytes to transmit.
347 */
348 public static function escpos_payload( string $type, string $value ): string {
349 if ( self::DEFAULT_SYMBOLOGY !== self::normalize_linear( $type ) ) {
350 return substr( $value, 0, self::MAX_DATA_BYTES );
351 }
352
353 $payload = substr( self::escpos_code128_data( $value ), 0, self::MAX_DATA_BYTES );
354
355 // Clamping can split an escaped `{{` pair; a lone trailing `{` would be
356 // read as the start of a code-set selector, so drop it.
357 $trailing_braces = \strlen( $payload ) - \strlen( rtrim( $payload, '{' ) );
358 if ( 1 === $trailing_braces % 2 ) {
359 $payload = substr( $payload, 0, -1 );
360 }
361
362 return $payload;
363 }
364
365 /**
366 * Build the ePOS-Print XML payload for a barcode value.
367 *
368 * Code 128 uses the same `{B` selector and `{{` escaping as the ESC/POS
369 * lane — the ePOS-Print spec defines the data identically. The branch is
370 * keyed on the name ePOS will receive, not on normalize_linear(): that
371 * folds every unmodelled type to Code 128, which would hand `{B` to the
372 * pass-through symbologies in EPOS_XML_ONLY_TYPES (`jan13`, `gs1_128`,
373 * `code128_auto`, …) and silently blank them.
374 *
375 * ePOS barcode data also has its own escape layer on top of the XML text:
376 * `\xnn` is a control byte and `\\` a literal backslash (ePOS-Print XML
377 * manual, <barcode> "escape sequences"). A raw backslash in a value would
378 * therefore be decoded by the printer — `A\x41` scans as `AA` — so every
379 * backslash is doubled first, for every symbology.
380 *
381 * Unlike escpos_payload() there is no MAX_DATA_BYTES clamp here: the
382 * emitter validates the value up front with the ESC/POS rules and falls
383 * back to text, and the data travels as XML rather than a length-prefixed
384 * byte command, so a clamp could only ever produce a wrong-but-scannable
385 * symbol.
386 *
387 * @param string $type The barcode type attribute value.
388 * @param string $value The barcode value.
389 *
390 * @return string The XML barcode payload.
391 */
392 public static function epos_xml_payload( string $type, string $value ): string {
393 $value = self::epos_xml_escape_data( $value );
394
395 if ( self::DEFAULT_SYMBOLOGY === self::epos_xml_name( $type ) ) {
396 return self::escpos_code128_data( $value );
397 }
398
399 return $value;
400 }
401
402 /**
403 * Apply the ePOS-Print barcode/symbol data escape layer to a value.
404 *
405 * Shared by <barcode> and <symbol> (QR) data, which use the same
406 * `\xnn` / `\\` escape sequences.
407 *
408 * @param string $value The raw value.
409 *
410 * @return string The value with literal backslashes doubled.
411 */
412 public static function epos_xml_escape_data( string $value ): string {
413 return str_replace( '\\', '\\\\', $value );
414 }
415
416 /**
417 * Map a symbology to its StarPRNT `ESC b n1` selector.
418 *
419 * @param string $type The barcode type attribute value.
420 *
421 * @return int The `n1` parameter byte.
422 */
423 public static function starprnt_id( string $type ): int {
424 return self::STARPRNT_IDS[ self::normalize_linear( $type ) ] ?? self::STARPRNT_IDS[ self::DEFAULT_SYMBOLOGY ];
425 }
426
427 /**
428 * Build the StarPRNT data bytes for a barcode value.
429 *
430 * StarPRNT escapes Code 128 with `%`, not `{`: `%0` is a literal `%`, and
431 * `%6`/`%7`/`%8` select code sets A/B/C. Omitting the start code is legal —
432 * the printer auto-selects — so, unlike ESC/POS, no selector is prefixed.
433 *
434 * @param string $type The barcode type attribute value.
435 * @param string $value The barcode value.
436 *
437 * @return string The data bytes to transmit.
438 */
439 public static function starprnt_payload( string $type, string $value ): string {
440 if ( self::DEFAULT_SYMBOLOGY !== self::normalize_linear( $type ) ) {
441 return substr( $value, 0, self::MAX_DATA_BYTES );
442 }
443
444 $payload = substr( self::starprnt_code128_data( $value ), 0, self::MAX_DATA_BYTES );
445
446 // Clamping can split an escaped `%0` pair. Star reads `%` plus the byte
447 // after it as an escape, and the next byte on the wire is the RS that
448 // terminates the barcode — so a lone trailing `%` eats the terminator and
449 // the printer keeps consuming the rest of the receipt as barcode data.
450 // Unlike an unprintable symbol, that failure is not self-limiting.
451 $trailing_percents = \strlen( $payload ) - \strlen( rtrim( $payload, '%' ) );
452 if ( 1 === $trailing_percents % 2 ) {
453 $payload = substr( $payload, 0, -1 );
454 }
455
456 return $payload;
457 }
458
459 /**
460 * Map a symbology to its Star Document Markup name.
461 *
462 * @param string $type The barcode type attribute value.
463 *
464 * @return string The `[barcode: type ...]` name.
465 */
466 public static function star_markup_name( string $type ): string {
467 $symbology = self::normalize_linear( $type );
468
469 return self::STAR_MARKUP_NAMES[ $symbology ] ?? $symbology;
470 }
471
472 /**
473 * Map a symbology to its ePOS-Print XML `<barcode type>` value.
474 *
475 * @param string $type The barcode type attribute value.
476 *
477 * @return string The attribute value.
478 */
479 public static function epos_xml_name( string $type ): string {
480 $requested = strtolower( trim( $type ) );
481
482 // ePOS-Print accepts symbologies WCPOS does not model, and the `type`
483 // attribute is free-form, so a template may legitimately ask for one.
484 // Folding those to Code 128 would silently downgrade a working GS1-128
485 // to a scannable-but-wrong symbol, so anything ePOS itself accepts is
486 // passed straight through. A name neither we nor ePOS recognise still
487 // falls back to Code 128 rather than being handed to the printer, which
488 // would reject the element and print nothing at all.
489 if ( \in_array( $requested, self::EPOS_XML_ONLY_TYPES, true ) ) {
490 return $requested;
491 }
492
493 $symbology = self::normalize_linear( $type );
494
495 return self::EPOS_XML_NAMES[ $symbology ] ?? $symbology;
496 }
497
498 /**
499 * Map a symbology to its picqer generator constant.
500 *
501 * @param string $type The barcode type attribute value.
502 *
503 * @return string The picqer TYPE_* constant value.
504 */
505 public static function picqer_type( string $type ): string {
506 $map = array(
507 'code128' => BarcodeGeneratorPNG::TYPE_CODE_128,
508 'code39' => BarcodeGeneratorPNG::TYPE_CODE_39,
509 'code93' => BarcodeGeneratorPNG::TYPE_CODE_93,
510 'ean13' => BarcodeGeneratorPNG::TYPE_EAN_13,
511 'ean8' => BarcodeGeneratorPNG::TYPE_EAN_8,
512 'upca' => BarcodeGeneratorPNG::TYPE_UPC_A,
513 'upce' => BarcodeGeneratorPNG::TYPE_UPC_E,
514 'codabar' => BarcodeGeneratorPNG::TYPE_CODABAR,
515 'itf' => BarcodeGeneratorPNG::TYPE_INTERLEAVED_2_5,
516 );
517
518 return $map[ self::normalize_linear( $type ) ] ?? $map[ self::DEFAULT_SYMBOLOGY ];
519 }
520
521 /**
522 * Build the unclamped ESC/POS Code 128 data bytes for a value.
523 *
524 * Split out from escpos_payload() so is_valid_value() can measure the
525 * *encoded* length. The selector and the doubled braces both count toward
526 * the 255-byte limit, so a value that fits before escaping can overflow
527 * after it — and the clamp would then print a shortened barcode that scans
528 * cleanly as the wrong value.
529 *
530 * @param string $value The barcode value.
531 *
532 * @return string The encoded data, before any length clamp.
533 */
534 private static function escpos_code128_data( string $value ): string {
535 return self::ESCPOS_CODE128_SELECTOR . str_replace( '{', '{{', $value );
536 }
537
538 /**
539 * Build the unclamped StarPRNT Code 128 data bytes for a value.
540 *
541 * @param string $value The barcode value.
542 *
543 * @return string The encoded data, before any length clamp.
544 */
545 private static function starprnt_code128_data( string $value ): string {
546 return str_replace( '%', '%0', $value );
547 }
548
549 /**
550 * Whether a value can be encoded as Code 128 on a given lane.
551 *
552 * Two lane-specific constraints, both of which fail silently on the printer:
553 *
554 * - Alphabet. escpos_payload() always selects code set B, which encodes
555 * ASCII 32-126 only; a tab, LF or CR needs set A, so an ESC/POS printer
556 * drops the symbol. StarPRNT auto-selects its code set, but its barcode
557 * data is RS-terminated, so control bytes cannot safely travel in it.
558 * - Length. The limit applies to the encoded bytes, not the merchant's
559 * value: ESC/POS adds a two-byte selector and doubles every `{`, StarPRNT
560 * doubles every `%`. A value that only overflows once escaped would be
561 * clamped into a shorter barcode that still scans — as the wrong value —
562 * so it is rejected here and printed as text instead.
563 *
564 * Epson's minimum of n >= 2 counts the `{B` selector, so a one-character
565 * value is legal on the wire (n = 3).
566 *
567 * @param string $value The barcode value.
568 * @param string $lane Lane discriminator: self::LANE_ESCPOS or self::LANE_STARPRNT.
569 *
570 * @return bool True when the value can be encoded.
571 */
572 private static function is_valid_code128( string $value, string $lane ): bool {
573 if ( self::LANE_ESCPOS === $lane ) {
574 if ( 1 !== preg_match( '/\A[\x20-\x7e]+\z/', $value ) ) {
575 return false;
576 }
577
578 return \strlen( self::escpos_code128_data( $value ) ) <= self::MAX_DATA_BYTES;
579 }
580
581 if ( '' === $value || ! self::is_ascii( $value ) || 1 === preg_match( '/[\x00-\x1f\x7f]/', $value ) ) {
582 return false;
583 }
584
585 return \strlen( self::starprnt_code128_data( $value ) ) <= self::MAX_DATA_BYTES;
586 }
587
588 /**
589 * Whether a value can be encoded as Code 39.
590 *
591 * `*` is the start/stop sentinel, not data. A printer that finds one in the
592 * middle of the value ends the symbol there, so `AB*CD` scans back as `AB`.
593 * A matching leading and trailing pair is accepted because that is how a
594 * value copied off another system's barcode is usually written; anything
595 * else is rejected and printed as text.
596 *
597 * @param string $value The barcode value.
598 *
599 * @return bool True when the value can be encoded.
600 */
601 private static function is_valid_code39( string $value ): bool {
602 $length = \strlen( $value );
603 if ( $length < 1 || $length > 255 ) {
604 return false;
605 }
606
607 $body = $value;
608 if ( $length >= 2 && '*' === $value[0] && '*' === $value[ $length - 1 ] ) {
609 $body = substr( $value, 1, -1 );
610 }
611
612 return 1 === preg_match( '/\A[0-9A-Z \$%\+\-\.\/]+\z/', $body );
613 }
614
615 /**
616 * Whether a GTIN's trailing digit is the correct mod-10 check digit.
617 *
618 * EAN-13, EAN-8 and UPC-A share one rule: weight the digits 3 and 1
619 * alternately from the one immediately left of the check digit, then the
620 * check digit is whatever brings the total to a multiple of ten. UPC-E's
621 * full 12-digit form is a UPC-A payload, so it uses the same rule.
622 *
623 * A wrong check digit is not a harmless typo. The printer either drops the
624 * symbol or prints one encoding a different number from the human-readable
625 * value beside it, and both outcomes are worse than the text fallback.
626 *
627 * @param string $digits An all-digit value whose last character is the check digit.
628 *
629 * @return bool True when the check digit is correct.
630 */
631 private static function has_valid_gtin_check_digit( string $digits ): bool {
632 $length = \strlen( $digits );
633 if ( $length < 2 ) {
634 return false;
635 }
636
637 $sum = 0;
638 for ( $index = $length - 2; $index >= 0; $index-- ) {
639 $weight = 0 === ( $length - 2 - $index ) % 2 ? 3 : 1;
640 $sum += (int) $digits[ $index ] * $weight;
641 }
642
643 return ( ( 10 - ( $sum % 10 ) ) % 10 ) === (int) $digits[ $length - 1 ];
644 }
645
646 /**
647 * Normalize to a 1D symbology, folding QR onto the default.
648 *
649 * The lane accessors are only ever reached for `barcode` AST nodes; a QR type
650 * arriving here means a caller routed a node wrongly, and Code 128 keeps the
651 * value printable rather than emitting an unencodable symbology id.
652 *
653 * @param string $type The barcode type attribute value.
654 *
655 * @return string A member of self::SYMBOLOGIES.
656 */
657 private static function normalize_linear( string $type ): string {
658 $symbology = self::normalize( $type );
659
660 return self::QRCODE === $symbology ? self::DEFAULT_SYMBOLOGY : $symbology;
661 }
662
663 /**
664 * Whether a value is a non-empty run of ASCII digits.
665 *
666 * @param string $value The value to test.
667 *
668 * @return bool True when the value is all digits.
669 */
670 private static function is_digits( string $value ): bool {
671 return 1 === preg_match( '/\A[0-9]+\z/', $value );
672 }
673
674 /**
675 * Whether every byte of a value is in the 0-127 range printers accept.
676 *
677 * @param string $value The value to test.
678 *
679 * @return bool True when the value is 7-bit clean.
680 */
681 private static function is_ascii( string $value ): bool {
682 return 1 !== preg_match( '/[\x80-\xff]/', $value );
683 }
684 }
685