| 1 |
<?php |
| 2 |
/** |
| 3 |
* Thermal Attribute Bounds. |
| 4 |
* |
| 5 |
* The one table of legal ranges for the numeric attributes of thermal markup. |
| 6 |
* |
| 7 |
* A thermal template is authored once and rendered down six paths: the merchant |
| 8 |
* preview (packages/thermal-utils/src/thermal-renderer.ts), the PDF receipt |
| 9 |
* (Html_Thermal_Emitter) and the four wire emitters (Escpos, Starprnt, Epos_Xml, |
| 10 |
* Star_Markup). A bound that holds in only some of them is not a safety net, it |
| 11 |
* is a divergence: the merchant previews 50 blank lines and the printer spools |
| 12 |
* 500. So every shared bound lives here, Thermal_Markup_Parser applies it once |
| 13 |
* while building the AST, and the emitters re-apply it to stay safe against |
| 14 |
* hand-built ASTs rather than inventing numbers of their own. |
| 15 |
* |
| 16 |
* Bounds that belong to one device rather than to the markup stay in that |
| 17 |
* emitter, next to the command they constrain, and say so — Starprnt's 8-module |
| 18 |
* QR ceiling and its 8-dot minimum barcode height are the current examples. |
| 19 |
* |
| 20 |
* Keep in step with THERMAL_BOUNDS in |
| 21 |
* packages/thermal-utils/src/thermal-renderer.ts, which mirrors this table. |
| 22 |
* |
| 23 |
* @author Paul Kilmurray <[email protected]> |
| 24 |
* |
| 25 |
* @see http://wcpos.com |
| 26 |
* @package WCPOS\WooCommercePOS |
| 27 |
*/ |
| 28 |
|
| 29 |
namespace WCPOS\WooCommercePOS\Templates\Thermal; |
| 30 |
|
| 31 |
/** |
| 32 |
* Thermal_Bounds class. |
| 33 |
*/ |
| 34 |
final class Thermal_Bounds { |
| 35 |
|
| 36 |
/** |
| 37 |
* Narrowest receipt in character columns, for every lane. |
| 38 |
* |
| 39 |
* Deliberately 1, not the 16 the PDF page uses. This bound is applied by the |
| 40 |
* PARSER, so it reaches every emitter — including Text_Thermal_Emitter, which |
| 41 |
* renders plain text with no physical roll behind it and has always accepted |
| 42 |
* narrow widths (`paper-width="10"` is pinned by its own tests). Raising the |
| 43 |
* shared floor to 16 silently re-centred that output. |
| 44 |
* |
| 45 |
* The rule this PR is built on is that a bound holds on every lane or none. |
| 46 |
* A floor of 16 is real, but it belongs to the medium that has a page, not to |
| 47 |
* the markup — so it stays where it already was ({@see self::PAPER_WIDTH_PDF_MIN}). |
| 48 |
* The hazard this PR exists to close is the top end, not the bottom. |
| 49 |
*/ |
| 50 |
public const PAPER_WIDTH_MIN = 1; |
| 51 |
|
| 52 |
/** |
| 53 |
* Narrowest receipt the PDF/preview page will lay out. |
| 54 |
* |
| 55 |
* Pre-existing behaviour of Html_Thermal_Emitter, preserved rather than |
| 56 |
* generalised: below this its character-cell arithmetic has nothing to divide. |
| 57 |
*/ |
| 58 |
public const PAPER_WIDTH_PDF_MIN = 16; |
| 59 |
|
| 60 |
/** |
| 61 |
* Widest receipt in character columns. |
| 62 |
* |
| 63 |
* 120 covers the widest thermal and impact rolls sold; the character grid |
| 64 |
* stops being a receipt beyond it. |
| 65 |
*/ |
| 66 |
public const PAPER_WIDTH_MAX = 120; |
| 67 |
|
| 68 |
/** |
| 69 |
* Smallest text size multiplier (normal size). |
| 70 |
*/ |
| 71 |
public const SIZE_MULTIPLIER_MIN = 1; |
| 72 |
|
| 73 |
/** |
| 74 |
* Largest text size multiplier. |
| 75 |
* |
| 76 |
* The ESC/POS `GS ! n` size byte carries one nibble per axis, so 8x is the |
| 77 |
* ceiling the hardware can express; Star's magnification tops out lower |
| 78 |
* still. Html_Thermal_Emitter renders the same 8em maximum. |
| 79 |
*/ |
| 80 |
public const SIZE_MULTIPLIER_MAX = 8; |
| 81 |
|
| 82 |
/** |
| 83 |
* Shortest 1D barcode, in dots. |
| 84 |
*/ |
| 85 |
public const BARCODE_HEIGHT_MIN = 1; |
| 86 |
|
| 87 |
/** |
| 88 |
* Tallest 1D barcode, in dots. |
| 89 |
* |
| 90 |
* `GS h n` is a single byte. |
| 91 |
*/ |
| 92 |
public const BARCODE_HEIGHT_MAX = 255; |
| 93 |
|
| 94 |
/** |
| 95 |
* Smallest QR module size. |
| 96 |
*/ |
| 97 |
public const QRCODE_SIZE_MIN = 1; |
| 98 |
|
| 99 |
/** |
| 100 |
* Largest QR module size. |
| 101 |
* |
| 102 |
* The ESC/POS `GS ( k` module-size function accepts 1-16. |
| 103 |
*/ |
| 104 |
public const QRCODE_SIZE_MAX = 16; |
| 105 |
|
| 106 |
/** |
| 107 |
* Narrowest image, in printer dots. |
| 108 |
*/ |
| 109 |
public const IMAGE_WIDTH_DOTS_MIN = 1; |
| 110 |
|
| 111 |
/** |
| 112 |
* Widest image, in printer dots. |
| 113 |
* |
| 114 |
* Comfortably past the 576-dot budget of an 80mm head, so it never truncates |
| 115 |
* a real logo, while still bounding the em width handed to Dompdf. |
| 116 |
*/ |
| 117 |
public const IMAGE_WIDTH_DOTS_MAX = 2000; |
| 118 |
|
| 119 |
/** |
| 120 |
* Fewest lines a `<feed>` advances. |
| 121 |
*/ |
| 122 |
public const FEED_LINES_MIN = 1; |
| 123 |
|
| 124 |
/** |
| 125 |
* Most lines a `<feed>` advances. |
| 126 |
* |
| 127 |
* At roughly 3.5mm per line this is ~17cm of blank paper — already far more |
| 128 |
* than any tear-off or cut gap a template legitimately wants, so a larger |
| 129 |
* number is a typo, and an unbounded one is a hazard: every wire emitter |
| 130 |
* turns `lines` straight into a loop or a str_repeat(). This is also the |
| 131 |
* bound the PDF path has shipped since it was written, so pinning the rest |
| 132 |
* of the paths to 50 moves the fewest of them. |
| 133 |
*/ |
| 134 |
public const FEED_LINES_MAX = 50; |
| 135 |
|
| 136 |
/** |
| 137 |
* Narrowest fixed column, in characters. |
| 138 |
* |
| 139 |
* A zero-width column would silently delete a semantic cell from the row. |
| 140 |
*/ |
| 141 |
public const COL_WIDTH_MIN = 1; |
| 142 |
|
| 143 |
/** |
| 144 |
* Widest fixed column, in characters. |
| 145 |
* |
| 146 |
* A column cannot outgrow the widest paper. |
| 147 |
*/ |
| 148 |
public const COL_WIDTH_MAX = self::PAPER_WIDTH_MAX; |
| 149 |
|
| 150 |
/** |
| 151 |
* Printable dots across 80 mm paper at 203 dpi. |
| 152 |
*/ |
| 153 |
public const DOTS_80MM = 576; |
| 154 |
|
| 155 |
/** |
| 156 |
* Printable dots across 58 mm paper at 203 dpi. |
| 157 |
*/ |
| 158 |
public const DOTS_58MM = 384; |
| 159 |
|
| 160 |
/** |
| 161 |
* Column count at or above which paper is treated as 80 mm. |
| 162 |
* |
| 163 |
* 58 mm rolls carry 32 columns; 80 mm rolls carry 42 or 48. |
| 164 |
*/ |
| 165 |
public const WIDE_PAPER_COLUMNS = 40; |
| 166 |
|
| 167 |
/** |
| 168 |
* The printable width of the roll, in dots. |
| 169 |
* |
| 170 |
* Templates size images in dots, but declare paper in character columns, so |
| 171 |
* every lane that puts an image on paper has to bridge the two. Shared here |
| 172 |
* because an image scaled against 576 dots on one lane and 384 on another is |
| 173 |
* the same divergence this whole table exists to prevent. |
| 174 |
* |
| 175 |
* @param int $columns The paper width in character columns. |
| 176 |
* |
| 177 |
* @return int The printable width in dots. |
| 178 |
*/ |
| 179 |
public static function paper_dots( int $columns ): int { |
| 180 |
return $columns >= self::WIDE_PAPER_COLUMNS ? self::DOTS_80MM : self::DOTS_58MM; |
| 181 |
} |
| 182 |
|
| 183 |
/** |
| 184 |
* Clamp a value into one of the ranges above. |
| 185 |
* |
| 186 |
* Thermal_Markup_Parser already clamps every attribute on its way into the |
| 187 |
* AST, so for parsed templates this is a no-op. It is here for ASTs built by |
| 188 |
* hand — tests, and any future caller that skips the parser — because a |
| 189 |
* bound the emitters do not enforce themselves is a bound that stops holding |
| 190 |
* the moment someone builds a node directly. |
| 191 |
* |
| 192 |
* @param mixed $value The candidate value. |
| 193 |
* @param int $fallback The fallback for missing/non-numeric values. |
| 194 |
* @param int $min The lowest legal value. |
| 195 |
* @param int $max The highest legal value. |
| 196 |
* |
| 197 |
* @return int The clamped integer. |
| 198 |
*/ |
| 199 |
public static function clamp_int( $value, int $fallback, int $min, int $max ): int { |
| 200 |
if ( ! is_numeric( $value ) ) { |
| 201 |
return $fallback; |
| 202 |
} |
| 203 |
|
| 204 |
return max( $min, min( $max, (int) $value ) ); |
| 205 |
} |
| 206 |
} |
| 207 |
|