PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.10.1
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.10.1
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.1, at includes/Templates/Barcode_Symbology.php

634 lines 21.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 * Map a symbology to its StarPRNT `ESC b n1` selector.
367 *
368 * @param string $type The barcode type attribute value.
369 *
370 * @return int The `n1` parameter byte.
371 */
372 public static function starprnt_id( string $type ): int {
373 return self::STARPRNT_IDS[ self::normalize_linear( $type ) ] ?? self::STARPRNT_IDS[ self::DEFAULT_SYMBOLOGY ];
374 }
375
376 /**
377 * Build the StarPRNT data bytes for a barcode value.
378 *
379 * StarPRNT escapes Code 128 with `%`, not `{`: `%0` is a literal `%`, and
380 * `%6`/`%7`/`%8` select code sets A/B/C. Omitting the start code is legal —
381 * the printer auto-selects — so, unlike ESC/POS, no selector is prefixed.
382 *
383 * @param string $type The barcode type attribute value.
384 * @param string $value The barcode value.
385 *
386 * @return string The data bytes to transmit.
387 */
388 public static function starprnt_payload( string $type, string $value ): string {
389 if ( self::DEFAULT_SYMBOLOGY !== self::normalize_linear( $type ) ) {
390 return substr( $value, 0, self::MAX_DATA_BYTES );
391 }
392
393 $payload = substr( self::starprnt_code128_data( $value ), 0, self::MAX_DATA_BYTES );
394
395 // Clamping can split an escaped `%0` pair. Star reads `%` plus the byte
396 // after it as an escape, and the next byte on the wire is the RS that
397 // terminates the barcode — so a lone trailing `%` eats the terminator and
398 // the printer keeps consuming the rest of the receipt as barcode data.
399 // Unlike an unprintable symbol, that failure is not self-limiting.
400 $trailing_percents = \strlen( $payload ) - \strlen( rtrim( $payload, '%' ) );
401 if ( 1 === $trailing_percents % 2 ) {
402 $payload = substr( $payload, 0, -1 );
403 }
404
405 return $payload;
406 }
407
408 /**
409 * Map a symbology to its Star Document Markup name.
410 *
411 * @param string $type The barcode type attribute value.
412 *
413 * @return string The `[barcode: type ...]` name.
414 */
415 public static function star_markup_name( string $type ): string {
416 $symbology = self::normalize_linear( $type );
417
418 return self::STAR_MARKUP_NAMES[ $symbology ] ?? $symbology;
419 }
420
421 /**
422 * Map a symbology to its ePOS-Print XML `<barcode type>` value.
423 *
424 * @param string $type The barcode type attribute value.
425 *
426 * @return string The attribute value.
427 */
428 public static function epos_xml_name( string $type ): string {
429 $requested = strtolower( trim( $type ) );
430
431 // ePOS-Print accepts symbologies WCPOS does not model, and the `type`
432 // attribute is free-form, so a template may legitimately ask for one.
433 // Folding those to Code 128 would silently downgrade a working GS1-128
434 // to a scannable-but-wrong symbol, so anything ePOS itself accepts is
435 // passed straight through. A name neither we nor ePOS recognise still
436 // falls back to Code 128 rather than being handed to the printer, which
437 // would reject the element and print nothing at all.
438 if ( \in_array( $requested, self::EPOS_XML_ONLY_TYPES, true ) ) {
439 return $requested;
440 }
441
442 $symbology = self::normalize_linear( $type );
443
444 return self::EPOS_XML_NAMES[ $symbology ] ?? $symbology;
445 }
446
447 /**
448 * Map a symbology to its picqer generator constant.
449 *
450 * @param string $type The barcode type attribute value.
451 *
452 * @return string The picqer TYPE_* constant value.
453 */
454 public static function picqer_type( string $type ): string {
455 $map = array(
456 'code128' => BarcodeGeneratorPNG::TYPE_CODE_128,
457 'code39' => BarcodeGeneratorPNG::TYPE_CODE_39,
458 'code93' => BarcodeGeneratorPNG::TYPE_CODE_93,
459 'ean13' => BarcodeGeneratorPNG::TYPE_EAN_13,
460 'ean8' => BarcodeGeneratorPNG::TYPE_EAN_8,
461 'upca' => BarcodeGeneratorPNG::TYPE_UPC_A,
462 'upce' => BarcodeGeneratorPNG::TYPE_UPC_E,
463 'codabar' => BarcodeGeneratorPNG::TYPE_CODABAR,
464 'itf' => BarcodeGeneratorPNG::TYPE_INTERLEAVED_2_5,
465 );
466
467 return $map[ self::normalize_linear( $type ) ] ?? $map[ self::DEFAULT_SYMBOLOGY ];
468 }
469
470 /**
471 * Build the unclamped ESC/POS Code 128 data bytes for a value.
472 *
473 * Split out from escpos_payload() so is_valid_value() can measure the
474 * *encoded* length. The selector and the doubled braces both count toward
475 * the 255-byte limit, so a value that fits before escaping can overflow
476 * after it — and the clamp would then print a shortened barcode that scans
477 * cleanly as the wrong value.
478 *
479 * @param string $value The barcode value.
480 *
481 * @return string The encoded data, before any length clamp.
482 */
483 private static function escpos_code128_data( string $value ): string {
484 return self::ESCPOS_CODE128_SELECTOR . str_replace( '{', '{{', $value );
485 }
486
487 /**
488 * Build the unclamped StarPRNT Code 128 data bytes for a value.
489 *
490 * @param string $value The barcode value.
491 *
492 * @return string The encoded data, before any length clamp.
493 */
494 private static function starprnt_code128_data( string $value ): string {
495 return str_replace( '%', '%0', $value );
496 }
497
498 /**
499 * Whether a value can be encoded as Code 128 on a given lane.
500 *
501 * Two lane-specific constraints, both of which fail silently on the printer:
502 *
503 * - Alphabet. escpos_payload() always selects code set B, which encodes
504 * ASCII 32-126 only; a tab, LF or CR needs set A, so an ESC/POS printer
505 * drops the symbol. StarPRNT auto-selects its code set, but its barcode
506 * data is RS-terminated, so control bytes cannot safely travel in it.
507 * - Length. The limit applies to the encoded bytes, not the merchant's
508 * value: ESC/POS adds a two-byte selector and doubles every `{`, StarPRNT
509 * doubles every `%`. A value that only overflows once escaped would be
510 * clamped into a shorter barcode that still scans — as the wrong value —
511 * so it is rejected here and printed as text instead.
512 *
513 * Epson's minimum of n >= 2 counts the `{B` selector, so a one-character
514 * value is legal on the wire (n = 3).
515 *
516 * @param string $value The barcode value.
517 * @param string $lane Lane discriminator: self::LANE_ESCPOS or self::LANE_STARPRNT.
518 *
519 * @return bool True when the value can be encoded.
520 */
521 private static function is_valid_code128( string $value, string $lane ): bool {
522 if ( self::LANE_ESCPOS === $lane ) {
523 if ( 1 !== preg_match( '/\A[\x20-\x7e]+\z/', $value ) ) {
524 return false;
525 }
526
527 return \strlen( self::escpos_code128_data( $value ) ) <= self::MAX_DATA_BYTES;
528 }
529
530 if ( '' === $value || ! self::is_ascii( $value ) || 1 === preg_match( '/[\x00-\x1f\x7f]/', $value ) ) {
531 return false;
532 }
533
534 return \strlen( self::starprnt_code128_data( $value ) ) <= self::MAX_DATA_BYTES;
535 }
536
537 /**
538 * Whether a value can be encoded as Code 39.
539 *
540 * `*` is the start/stop sentinel, not data. A printer that finds one in the
541 * middle of the value ends the symbol there, so `AB*CD` scans back as `AB`.
542 * A matching leading and trailing pair is accepted because that is how a
543 * value copied off another system's barcode is usually written; anything
544 * else is rejected and printed as text.
545 *
546 * @param string $value The barcode value.
547 *
548 * @return bool True when the value can be encoded.
549 */
550 private static function is_valid_code39( string $value ): bool {
551 $length = \strlen( $value );
552 if ( $length < 1 || $length > 255 ) {
553 return false;
554 }
555
556 $body = $value;
557 if ( $length >= 2 && '*' === $value[0] && '*' === $value[ $length - 1 ] ) {
558 $body = substr( $value, 1, -1 );
559 }
560
561 return 1 === preg_match( '/\A[0-9A-Z \$%\+\-\.\/]+\z/', $body );
562 }
563
564 /**
565 * Whether a GTIN's trailing digit is the correct mod-10 check digit.
566 *
567 * EAN-13, EAN-8 and UPC-A share one rule: weight the digits 3 and 1
568 * alternately from the one immediately left of the check digit, then the
569 * check digit is whatever brings the total to a multiple of ten. UPC-E's
570 * full 12-digit form is a UPC-A payload, so it uses the same rule.
571 *
572 * A wrong check digit is not a harmless typo. The printer either drops the
573 * symbol or prints one encoding a different number from the human-readable
574 * value beside it, and both outcomes are worse than the text fallback.
575 *
576 * @param string $digits An all-digit value whose last character is the check digit.
577 *
578 * @return bool True when the check digit is correct.
579 */
580 private static function has_valid_gtin_check_digit( string $digits ): bool {
581 $length = \strlen( $digits );
582 if ( $length < 2 ) {
583 return false;
584 }
585
586 $sum = 0;
587 for ( $index = $length - 2; $index >= 0; $index-- ) {
588 $weight = 0 === ( $length - 2 - $index ) % 2 ? 3 : 1;
589 $sum += (int) $digits[ $index ] * $weight;
590 }
591
592 return ( ( 10 - ( $sum % 10 ) ) % 10 ) === (int) $digits[ $length - 1 ];
593 }
594
595 /**
596 * Normalize to a 1D symbology, folding QR onto the default.
597 *
598 * The lane accessors are only ever reached for `barcode` AST nodes; a QR type
599 * arriving here means a caller routed a node wrongly, and Code 128 keeps the
600 * value printable rather than emitting an unencodable symbology id.
601 *
602 * @param string $type The barcode type attribute value.
603 *
604 * @return string A member of self::SYMBOLOGIES.
605 */
606 private static function normalize_linear( string $type ): string {
607 $symbology = self::normalize( $type );
608
609 return self::QRCODE === $symbology ? self::DEFAULT_SYMBOLOGY : $symbology;
610 }
611
612 /**
613 * Whether a value is a non-empty run of ASCII digits.
614 *
615 * @param string $value The value to test.
616 *
617 * @return bool True when the value is all digits.
618 */
619 private static function is_digits( string $value ): bool {
620 return 1 === preg_match( '/\A[0-9]+\z/', $value );
621 }
622
623 /**
624 * Whether every byte of a value is in the 0-127 range printers accept.
625 *
626 * @param string $value The value to test.
627 *
628 * @return bool True when the value is 7-bit clean.
629 */
630 private static function is_ascii( string $value ): bool {
631 return 1 !== preg_match( '/[\x80-\xff]/', $value );
632 }
633 }
634