PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.10.17
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.10.17
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 1.9.13 1.9.12 All 161 releases
woocommerce-pos / includes / Templates / Barcode_Image.php

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

346 lines 11.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Barcode / QR code image helper for the PDF render path.
4 *
5 * Dompdf cannot render inline `<svg>` barcodes (nor SVG embedded via an `<img>`
6 * data URI) — both come out blank, leaving only the order number as text. PNG
7 * raster images embedded as `<img>` data URIs render reliably, so this helper
8 * rasterizes 1D barcodes (picqer) and QR codes (chillerlan) to PNG and returns
9 * a self-contained `<img>` tag. It also rewrites `<barcode>` / `<qrcode>` markup
10 * (the logicless gallery template syntax) into those `<img>` tags, mirroring the
11 * client-side preview renderer so the PDF matches the on-screen receipt.
12 *
13 * @author Paul Kilmurray <paul@kilbot.com>
14 *
15 * @see http://wcpos.com
16 * @package WCPOS\WooCommercePOS\Templates
17 */
18
19 namespace WCPOS\WooCommercePOS\Templates;
20
21 use Throwable;
22 use WCPOS\Vendor\chillerlan\QRCode\Output\QROutputInterface;
23 use WCPOS\Vendor\chillerlan\QRCode\QRCode;
24 use WCPOS\Vendor\chillerlan\QRCode\QROptions;
25 use WCPOS\Vendor\Picqer\Barcode\BarcodeGeneratorPNG;
26
27 /**
28 * Barcode_Image class.
29 */
30 class Barcode_Image {
31
32 /**
33 * Build an `<img>` tag for a 1D barcode, or an empty string on failure.
34 *
35 * @param string $type The barcode symbology (e.g. code128, ean13).
36 * @param string $value The barcode value.
37 * @param int $height The barcode height in pixels.
38 *
39 * @return string The `<img>` tag, or '' when the value is empty or generation fails.
40 */
41 public static function barcode_img( string $type, string $value, int $height = 40 ): string {
42 $png = self::barcode_png( $type, $value, $height );
43 if ( '' === $png ) {
44 return '';
45 }
46
47 // bwip-js renders the human-readable value under the bars in the
48 // client-side previews; mirror it so the PDF matches on-screen.
49 return self::img_from_png( $png ) . self::barcode_text( trim( $value ) );
50 }
51
52 /**
53 * Rasterize a barcode to opaque PNG bytes.
54 *
55 * The receipt raster emitter composites these straight onto its canvas, so it
56 * needs the bytes rather than the `<img>` wrapper the HTML/PDF path uses.
57 *
58 * @param string $type The barcode symbology (e.g. code128, ean13).
59 * @param string $value The barcode value.
60 * @param int $height The barcode height in pixels.
61 *
62 * @return string The PNG bytes, or '' when the value is empty or generation fails.
63 */
64 public static function barcode_png( string $type, string $value, int $height = 40 ): string {
65 $text = trim( $value );
66 if ( '' === $text ) {
67 return '';
68 }
69
70 try {
71 $png = self::without_vendor_deprecations(
72 static function () use ( $text, $type, $height ): string {
73 $generator = new BarcodeGeneratorPNG();
74 // picqer defaults to Imagick when the extension is loaded, but a
75 // PNG-less Imagick build (seen on some hosts) throws. Prefer GD,
76 // which is near-universal and the recommended WordPress image lib.
77 if ( \function_exists( 'imagecreate' ) ) {
78 $generator->useGd();
79 }
80
81 // Clamp the height so a malformed template dimension cannot
82 // allocate a huge raster.
83 return $generator->getBarcode( $text, Barcode_Symbology::picqer_type( $type ), 2, max( 8, min( 600, $height ) ) );
84 }
85 );
86
87 return self::flatten_png( $png );
88 } catch ( Throwable $error ) {
89 return '';
90 }
91 }
92
93 /**
94 * Build the human-readable text line shown beneath a 1D barcode.
95 *
96 * @param string $value The barcode value.
97 *
98 * @return string The HTML fragment.
99 */
100 private static function barcode_text( string $value ): string {
101 return '<div style="text-align: center; font-family: \'DejaVu Sans Mono\', Menlo, Consolas, monospace; '
102 . 'font-size: 1em; letter-spacing: 2px; line-height: 1.3; margin-top: 1px">'
103 . esc_html( $value ) . '</div>';
104 }
105
106 /**
107 * Build an `<img>` tag for a QR code, or an empty string on failure.
108 *
109 * @param string $value The QR code value.
110 * @param int $size The QR module scale (pixels per module).
111 *
112 * @return string The `<img>` tag, or '' when the value is empty or generation fails.
113 */
114 public static function qrcode_img( string $value, int $size = 4 ): string {
115 $png = self::qrcode_png( $value, $size );
116
117 return '' === $png ? '' : self::img_from_png( $png );
118 }
119
120 /**
121 * Rasterize a QR code to opaque PNG bytes.
122 *
123 * @param string $value The QR code value.
124 * @param int $size The QR module scale (pixels per module).
125 *
126 * @return string The PNG bytes, or '' when the value is empty or generation fails.
127 */
128 public static function qrcode_png( string $value, int $size = 4 ): string {
129 $text = trim( $value );
130 if ( '' === $text ) {
131 return '';
132 }
133
134 try {
135 $png = self::without_vendor_deprecations(
136 static function () use ( $text, $size ): string {
137 $options = new QROptions(
138 array(
139 'outputType' => QROutputInterface::GDIMAGE_PNG,
140 'imageBase64' => false,
141 // Clamp the scale so a malformed template dimension cannot
142 // allocate a huge raster.
143 'scale' => max( 2, min( 40, $size ) ),
144 )
145 );
146
147 return ( new QRCode( $options ) )->render( $text );
148 }
149 );
150
151 return self::flatten_png( $png );
152 } catch ( Throwable $error ) {
153 return '';
154 }
155 }
156
157 /**
158 * Replace `<barcode>` / `<qrcode>` markup with opaque placeholder tokens.
159 *
160 * Each element that rasterizes successfully is swapped for a plain-text token,
161 * and its `<img>` tag is stored in `$images` keyed by that token. The caller is
162 * expected to sanitize the returned HTML and then splice the images back in
163 * (e.g. `strtr( wp_kses_post( $html ), $images )`). This keeps the `data:` image
164 * URI out of the sanitizer entirely, so no `data:` protocol allowance — which
165 * would widen the protocol allow-list for every URI attribute — is needed.
166 * Elements whose value is empty, or which fail to rasterize, are left untouched
167 * so the sanitizer can drop them.
168 *
169 * @param string $html The rendered template HTML.
170 * @param array $images Filled with token => `<img>` HTML for the caller to splice in.
171 *
172 * @return string The HTML with barcode markup replaced by placeholder tokens.
173 */
174 public static function replace_markup( string $html, array &$images ): string {
175 if ( false === stripos( $html, '<barcode' ) && false === stripos( $html, '<qrcode' ) ) {
176 return $html;
177 }
178
179 return (string) preg_replace_callback(
180 '#<(barcode|qrcode)\b([^>]*)>(.*?)</\1>#is',
181 static function ( array $match ) use ( &$images ): string {
182 $img = self::element_to_img( strtolower( $match[1] ), $match[2], $match[3] );
183 if ( '' === $img ) {
184 return $match[0];
185 }
186
187 $token = 'WCPOSBARCODEPLACEHOLDER' . \count( $images ) . 'X';
188 $images[ $token ] = $img;
189
190 return $token;
191 },
192 $html
193 );
194 }
195
196 /**
197 * Rasterize a single `<barcode>` / `<qrcode>` element to an `<img>` tag.
198 *
199 * Mirrors the client-side logicless preview renderer: the value is the
200 * `data-value` attribute or the text content; the symbology is the
201 * `data-barcode` or `type` attribute (a `<qrcode>` tag, a qr/qrcode type, or a
202 * typeless element all render a QR code, matching the preview).
203 *
204 * @param string $tag The lowercased tag name (barcode|qrcode).
205 * @param string $attrs The raw attribute string.
206 * @param string $inner The element's inner content.
207 *
208 * @return string The `<img>` tag, or '' when empty or rasterization fails.
209 */
210 private static function element_to_img( string $tag, string $attrs, string $inner ): string {
211 $value = self::attr( $attrs, 'data-value' );
212 $value = '' !== $value ? $value : trim( wp_strip_all_tags( $inner ) );
213 $value = html_entity_decode( $value, ENT_QUOTES | ENT_HTML5, 'UTF-8' );
214 if ( '' === trim( $value ) ) {
215 return '';
216 }
217
218 if ( 'qrcode' === $tag ) {
219 $raw_type = 'qrcode';
220 } else {
221 $raw_type = self::attr( $attrs, 'data-barcode' );
222 $raw_type = '' !== $raw_type ? $raw_type : self::attr( $attrs, 'type' );
223 $raw_type = '' !== $raw_type ? $raw_type : 'qr';
224 }
225 $type = strtolower( trim( $raw_type ) );
226
227 if ( Barcode_Symbology::is_qr( $type ) ) {
228 $size = (int) self::attr( $attrs, 'size' );
229
230 return self::qrcode_img( $value, $size > 0 ? $size : 4 );
231 }
232
233 $height = (int) self::attr( $attrs, 'height' );
234
235 return self::barcode_img( $type, $value, $height > 0 ? $height : 40 );
236 }
237
238 /**
239 * Wrap raw PNG bytes in an `<img>` data-URI tag constrained to the receipt width.
240 *
241 * @param string $png The raw PNG image bytes.
242 *
243 * @return string The `<img>` tag.
244 */
245 private static function img_from_png( string $png ): string {
246 // Callers hand over already-flattened bytes; flatten_png() is idempotent, so
247 // the second pass is a no-op that keeps this safe for any future caller.
248 return '<img src="data:image/png;base64,' . base64_encode( self::flatten_png( $png ) ) . '" '
249 . 'style="max-width: 100%; height: auto" alt="" />';
250 }
251
252 /**
253 * Flatten a PNG onto an opaque white background, dropping any alpha channel.
254 *
255 * The picqer barcode generator emits PNGs with a transparent background (some
256 * QR builds do too). Dompdf routes transparent PNGs through an Imagick
257 * alpha-extraction path that fails
258 * on hosts whose Imagick build lacks a PNG delegate (`Unable to set format`).
259 * An opaque PNG takes Dompdf's plain GD path instead, which is what receipts
260 * need anyway (black on white). Returns the input unchanged if GD is missing
261 * or the image cannot be decoded.
262 *
263 * @param string $png The raw PNG image bytes.
264 *
265 * @return string The opaque PNG bytes.
266 */
267 private static function flatten_png( string $png ): string {
268 if ( ! \function_exists( 'imagecreatefromstring' ) ) {
269 return $png;
270 }
271
272 // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- Invalid image data returns false rather than warning.
273 $source = @imagecreatefromstring( $png );
274 if ( false === $source ) {
275 return $png;
276 }
277
278 $width = imagesx( $source );
279 $height = imagesy( $source );
280 $canvas = imagecreatetruecolor( $width, $height );
281 $white = (int) imagecolorallocate( $canvas, 255, 255, 255 );
282 imagefilledrectangle( $canvas, 0, 0, $width, $height, $white );
283 imagealphablending( $canvas, true );
284 imagecopy( $canvas, $source, 0, 0, 0, 0, $width, $height );
285 imagesavealpha( $canvas, false );
286
287 ob_start();
288 imagepng( $canvas );
289 $flat = (string) ob_get_clean();
290
291 return '' !== $flat ? $flat : $png;
292 }
293
294 /**
295 * Read a single HTML attribute value out of an attribute string.
296 *
297 * @param string $attrs The raw attribute string (e.g. ` type="code128" height="40"`).
298 * @param string $name The attribute name.
299 *
300 * @return string The attribute value, or '' when absent.
301 */
302 private static function attr( string $attrs, string $name ): string {
303 if ( preg_match( '#\b' . preg_quote( $name, '#' ) . '\s*=\s*("([^"]*)"|\'([^\']*)\'|([^\s>]+))#i', $attrs, $m ) ) {
304 return ( $m[2] ?? '' ) . ( $m[3] ?? '' ) . ( $m[4] ?? '' );
305 }
306
307 return '';
308 }
309
310 /**
311 * Run a callable with E_DEPRECATED notices from the vendored libraries silenced.
312 *
313 * The prefixed picqer PNG generator calls imagedestroy(), deprecated on PHP
314 * 8.5; the notice is harmless but would otherwise spam logs on each receipt.
315 *
316 * @param callable $callback The callback to execute.
317 *
318 * @return string The callback's return value.
319 */
320 private static function without_vendor_deprecations( callable $callback ): string {
321 $previous_handler = null;
322 $previous_handler = set_error_handler(
323 static function ( int $errno, string $errstr, string $errfile = '', int $errline = 0 ) use ( &$previous_handler ): bool {
324 if (
325 0 !== ( $errno & ( E_DEPRECATED | E_USER_DEPRECATED ) )
326 && false !== strpos( str_replace( '\\', '/', $errfile ), '/vendor_prefixed/' )
327 ) {
328 return true;
329 }
330
331 if ( \is_callable( $previous_handler ) ) {
332 return (bool) \call_user_func( $previous_handler, $errno, $errstr, $errfile, $errline );
333 }
334
335 return false;
336 }
337 );
338
339 try {
340 return (string) $callback();
341 } finally {
342 restore_error_handler();
343 }
344 }
345 }
346