PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.10.4
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.10.4
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 / Thermal / Text_Thermal_Emitter.php

Text_Thermal_Emitter.php in WCPOS – Point of Sale (POS) plugin for WooCommerce 1.10.4, at includes/Templates/Thermal/Text_Thermal_Emitter.php

367 lines 10.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Plain Text Thermal Emitter Class.
4 *
5 * Emits `text/plain` from a thermal AST: the receipt's character-cell layout with
6 * every command byte removed. Star CloudPRNT lists `text/plain` as decodable on
7 * every model, including the Line Mode-only ones (TSP650II/TSP700II/TSP800II)
8 * that cannot decode StarPRNT at all, so this is the format that keeps a printer
9 * working when native emission is not on its menu.
10 *
11 * Because the format carries no commands, the peripherals move to the transport:
12 * cut and cash-drawer are requested with the `X-Star-Cut` / `X-Star-CashDrawer`
13 * response headers on the CloudPRNT job fetch. This emitter therefore swallows
14 * `<cut>` and `<drawer>` nodes and reports them through cut_type() and drawer()
15 * so the caller can set those headers.
16 *
17 * Two things the command formats can express are simply not expressible here,
18 * and both are limits of the protocol rather than of this code:
19 *
20 * - **Drawer connector.** `X-Star-CashDrawer` takes only `none`/`start`/`end`,
21 * so a job configured for the second connector (`pin5`) fires the printer's
22 * default drawer output instead. StarPRNT emits a connector-specific pulse;
23 * plain text has no way to ask for one.
24 * - **Character encoding.** `Starprnt_Thermal_Emitter` opens every job with the
25 * `ESC GS ) U` UTF-8 select sequence; a command-free format cannot send it, so
26 * non-ASCII text is decoded with whatever code page the printer is set to.
27 * normalize_text() folds the typographic characters receipts actually produce
28 * (smart quotes, dashes, no-break spaces) down to ASCII, which covers the
29 * common case; full glyph coverage is what the `image/png` fallback is for.
30 *
31 * Styling (`<bold>`, `<underline>`, `<invert>`, `<size>`) has no plain-text
32 * expression and is walked through transparently. `<barcode>` and `<qrcode>`
33 * cannot be rendered, so their value is printed as a centred text line — a
34 * receipt whose QR carries an order URL stays useful, where dropping the node
35 * would lose the information entirely. `<image>` is dropped, matching
36 * `Escpos_Thermal_Emitter`.
37 *
38 * @package WCPOS\WooCommercePOS\Templates\Thermal
39 */
40
41 namespace WCPOS\WooCommercePOS\Templates\Thermal;
42
43 /**
44 * Text_Thermal_Emitter class.
45 */
46 class Text_Thermal_Emitter {
47
48 /**
49 * Render options.
50 *
51 * @var array
52 */
53 private $options = array();
54
55 /**
56 * Accumulated output text.
57 *
58 * @var string
59 */
60 private $buffer = '';
61
62 /**
63 * The text buffered for the line currently being built.
64 *
65 * @var string
66 */
67 private $line = '';
68
69 /**
70 * The paper width in character columns.
71 *
72 * @var int
73 */
74 private $columns = 48;
75
76 /**
77 * The current alignment mode (left|center|right).
78 *
79 * @var string
80 */
81 private $align = 'left';
82
83 /**
84 * The cut requested by the AST, or null when it asked for none.
85 *
86 * @var string|null
87 */
88 private $cut_type = null;
89
90 /**
91 * The drawer kick requested by the AST/options, or null when none.
92 *
93 * @var string|null
94 */
95 private $drawer = null;
96
97 /**
98 * Constructor.
99 *
100 * @param array $options Render options.
101 */
102 public function __construct( array $options = array() ) {
103 $this->options = $options;
104 }
105
106 /**
107 * Emit plain text from a thermal AST.
108 *
109 * @param array $ast The thermal AST root (a receipt node).
110 *
111 * @return string The receipt as plain text.
112 */
113 public function emit( array $ast ): string {
114 $this->buffer = '';
115 $this->line = '';
116 $this->align = 'left';
117 $this->cut_type = null;
118 $this->drawer = null;
119
120 $this->columns = isset( $ast['paper_width'] ) ? max( 1, (int) $ast['paper_width'] ) : 48;
121
122 $children = isset( $ast['children'] ) && \is_array( $ast['children'] ) ? $ast['children'] : array();
123 $this->walk_nodes( $children );
124 $this->flush_line();
125
126 // The AST carries no drawer node, but the job asked for one: the transport
127 // header is the only place a text/plain job can request it.
128 if ( null === $this->drawer && ! empty( $this->options['auto_open_drawer'] ) ) {
129 $this->drawer = 'end';
130 }
131
132 return $this->buffer;
133 }
134
135 /**
136 * The cut the rendered AST asked for, for the `X-Star-Cut` header.
137 *
138 * @return string|null 'full', 'partial', or null when the receipt cuts nothing.
139 */
140 public function cut_type(): ?string {
141 return $this->cut_type;
142 }
143
144 /**
145 * The drawer kick the rendered job asked for, for `X-Star-CashDrawer`.
146 *
147 * @return string|null 'start', 'end', or null when no drawer should fire.
148 */
149 public function drawer(): ?string {
150 return $this->drawer;
151 }
152
153 /**
154 * Walk a list of AST nodes.
155 *
156 * @param array $nodes The AST nodes.
157 *
158 * @return void
159 */
160 private function walk_nodes( array $nodes ): void {
161 foreach ( $nodes as $node ) {
162 if ( \is_array( $node ) ) {
163 $this->walk_node( $node );
164 }
165 }
166 }
167
168 /**
169 * Walk a single AST node.
170 *
171 * @param array $node The AST node.
172 *
173 * @return void
174 */
175 private function walk_node( array $node ): void {
176 $type = isset( $node['type'] ) ? $node['type'] : '';
177 $children = isset( $node['children'] ) && \is_array( $node['children'] ) ? $node['children'] : array();
178
179 switch ( $type ) {
180 case 'raw-text':
181 $this->line .= Thermal_Text_Layout::normalize_text( isset( $node['value'] ) ? (string) $node['value'] : '' );
182 break;
183 case 'text':
184 $this->emit_text_line( $children );
185 break;
186 case 'bold':
187 case 'underline':
188 case 'invert':
189 case 'size':
190 // No plain-text expression; the children still print.
191 $this->walk_nodes( $children );
192 break;
193 case 'align':
194 $this->emit_align( $node );
195 break;
196 case 'row':
197 $this->emit_row( $node );
198 break;
199 case 'line':
200 $this->emit_rule( $node );
201 break;
202 case 'barcode':
203 case 'qrcode':
204 $this->emit_symbol_fallback( $node );
205 break;
206 case 'image':
207 // Dropped: a text/plain job cannot carry raster data.
208 break;
209 case 'cut':
210 $this->cut_type = isset( $node['cut_type'] ) ? (string) $node['cut_type'] : 'partial';
211 break;
212 case 'feed':
213 $this->emit_feed( $node );
214 break;
215 case 'drawer':
216 $this->drawer = 'end';
217 break;
218 case 'receipt':
219 $this->walk_nodes( $children );
220 break;
221 }
222 }
223
224 /**
225 * Emit a single printed text line, aligned within the paper width.
226 *
227 * @param array $children The child nodes of the text node.
228 *
229 * @return void
230 */
231 private function emit_text_line( array $children ): void {
232 if ( 'left' !== $this->align ) {
233 $plain = Thermal_Text_Layout::normalize_text( Thermal_Text_Layout::extract_text( $children ) );
234 $pad = Thermal_Text_Layout::alignment_padding( $this->align, Thermal_Text_Layout::display_width( $plain ), $this->columns );
235 if ( $pad > 0 ) {
236 $this->line .= str_repeat( ' ', $pad );
237 }
238 }
239 $this->walk_nodes( $children );
240 $this->newline();
241 }
242
243 /**
244 * Walk an alignment-wrapped block with the alignment applied.
245 *
246 * @param array $node The align AST node.
247 *
248 * @return void
249 */
250 private function emit_align( array $node ): void {
251 $previous = $this->align;
252 $this->align = isset( $node['mode'] ) ? (string) $node['mode'] : 'left';
253 $this->walk_nodes( isset( $node['children'] ) && \is_array( $node['children'] ) ? $node['children'] : array() );
254 $this->align = $previous;
255 }
256
257 /**
258 * Emit a row as one physical line followed by a newline.
259 *
260 * @param array $node The row AST node.
261 *
262 * @return void
263 */
264 private function emit_row( array $node ): void {
265 $cols = isset( $node['children'] ) && \is_array( $node['children'] ) ? $node['children'] : array();
266 $widths = Thermal_Text_Layout::resolve_row_widths( $cols, $this->columns );
267
268 $row = '';
269 foreach ( $cols as $index => $col ) {
270 $width = isset( $widths[ $index ] ) ? $widths[ $index ] : 1;
271 $text = Thermal_Text_Layout::normalize_text( Thermal_Text_Layout::extract_text( isset( $col['children'] ) ? $col['children'] : array() ) );
272 $text = Thermal_Text_Layout::truncate_display( $text, $width );
273 $pad = max( 0, $width - Thermal_Text_Layout::display_width( $text ) );
274 $align = isset( $col['align'] ) ? (string) $col['align'] : 'left';
275 if ( 'right' === $align ) {
276 $row .= str_repeat( ' ', $pad ) . $text;
277 } else {
278 $row .= $text . str_repeat( ' ', $pad );
279 }
280 }
281
282 $this->line .= $row;
283 $this->newline();
284 }
285
286 /**
287 * Emit a horizontal rule line.
288 *
289 * @param array $node The line AST node.
290 *
291 * @return void
292 */
293 private function emit_rule( array $node ): void {
294 $style = isset( $node['style'] ) ? (string) $node['style'] : 'single';
295
296 if ( 'dotted' === $style ) {
297 $pattern = '. ';
298 $repeat = (int) ceil( $this->columns / \strlen( $pattern ) );
299 $text = substr( str_repeat( $pattern, $repeat ), 0, $this->columns );
300 } elseif ( 'double' === $style ) {
301 $text = str_repeat( '=', $this->columns );
302 } else {
303 // single and dashed both render as '-' across the width.
304 $text = str_repeat( '-', $this->columns );
305 }
306
307 $this->line .= $text;
308 $this->newline();
309 }
310
311 /**
312 * Print a barcode/QR value as text, centred within the paper width.
313 *
314 * @param array $node The barcode or qrcode AST node.
315 *
316 * @return void
317 */
318 private function emit_symbol_fallback( array $node ): void {
319 $value = Thermal_Text_Layout::normalize_text( isset( $node['value'] ) ? (string) $node['value'] : '' );
320 if ( '' === $value ) {
321 return;
322 }
323
324 $pad = Thermal_Text_Layout::alignment_padding( 'center', Thermal_Text_Layout::display_width( $value ), $this->columns );
325 if ( $pad > 0 ) {
326 $this->line .= str_repeat( ' ', $pad );
327 }
328 $this->line .= $value;
329 $this->newline();
330 }
331
332 /**
333 * Emit a paper feed of N blank lines.
334 *
335 * @param array $node The feed AST node.
336 *
337 * @return void
338 */
339 private function emit_feed( array $node ): void {
340 $lines = isset( $node['lines'] ) ? max( 1, (int) $node['lines'] ) : 1;
341 for ( $index = 0; $index < $lines; $index++ ) {
342 $this->newline();
343 }
344 }
345
346 /**
347 * Close the current line and start a new one.
348 *
349 * @return void
350 */
351 private function newline(): void {
352 $this->buffer .= rtrim( $this->line, " \t" ) . "\n";
353 $this->line = '';
354 }
355
356 /**
357 * Flush any text buffered outside a line-terminating node.
358 *
359 * @return void
360 */
361 private function flush_line(): void {
362 if ( '' !== $this->line ) {
363 $this->newline();
364 }
365 }
366 }
367