PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.9.14
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.9.14
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_Image.php

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

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