| 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 |
|