| 1 |
<?php |
| 2 |
/** |
| 3 |
* Template PDF Service. |
| 4 |
* |
| 5 |
* Renders any receipt template to PDF bytes. Thermal templates are built into a |
| 6 |
* thermal AST and emitted to receipt HTML via Html_Thermal_Emitter; all other |
| 7 |
* engines (logicless, legacy-php) run their matching renderer and capture the |
| 8 |
* echoed HTML via output buffering. The resulting HTML is rasterized to PDF with |
| 9 |
* Pdf_Renderer (Dompdf). |
| 10 |
* |
| 11 |
* @author Paul Kilmurray <paul@kilbot.com> |
| 12 |
* |
| 13 |
* @see http://wcpos.com |
| 14 |
* @package WCPOS\WooCommercePOS\Services |
| 15 |
*/ |
| 16 |
|
| 17 |
namespace WCPOS\WooCommercePOS\Services; |
| 18 |
|
| 19 |
use WCPOS\WooCommercePOS\Templates\Thermal\Html_Thermal_Emitter; |
| 20 |
use WCPOS\WooCommercePOS\Templates\Thermal\Thermal_Renderer; |
| 21 |
use WC_Abstract_Order; |
| 22 |
|
| 23 |
/** |
| 24 |
* Template_Pdf_Service class. |
| 25 |
*/ |
| 26 |
class Template_Pdf_Service { |
| 27 |
|
| 28 |
/** |
| 29 |
* Render a receipt template for an order into PDF bytes. |
| 30 |
* |
| 31 |
* @param array $template Template metadata/content (must include 'engine'). |
| 32 |
* @param WC_Abstract_Order $order The order to render. |
| 33 |
* |
| 34 |
* @return string The PDF document bytes (begins with '%PDF-'). |
| 35 |
*/ |
| 36 |
public function render( array $template, WC_Abstract_Order $order ): string { |
| 37 |
$engine = isset( $template['engine'] ) ? (string) $template['engine'] : ''; |
| 38 |
|
| 39 |
$wp_overnight_pdf = $this->maybe_render_wp_overnight_pdf( $template, $order ); |
| 40 |
if ( null !== $wp_overnight_pdf ) { |
| 41 |
return $wp_overnight_pdf; |
| 42 |
} |
| 43 |
|
| 44 |
if ( 'thermal' === $engine ) { |
| 45 |
$paper_width_pt = $this->thermal_paper_width_pt( $template ); |
| 46 |
|
| 47 |
$ast = ( new Thermal_Renderer() )->build_ast( $template, $order ); |
| 48 |
$html = ( new Html_Thermal_Emitter() )->emit( |
| 49 |
$ast, |
| 50 |
array( |
| 51 |
// CSS pixels (96dpi) for the paper width, so the emitter can |
| 52 |
// scale the monospace character grid to fit the page. |
| 53 |
'paper_width_px' => $paper_width_pt * 4 / 3, |
| 54 |
) |
| 55 |
); |
| 56 |
|
| 57 |
// Narrow continuous-roll receipt page at the template's declared paper |
| 58 |
// width (58/80mm). Render on a tall probe page, then let Pdf_Renderer |
| 59 |
// fit the height to the content so downloaded PDFs and PrintNode PDFs |
| 60 |
// avoid blank tails/overflow pages. |
| 61 |
return ( new Pdf_Renderer() )->render_html( |
| 62 |
$html, |
| 63 |
array( |
| 64 |
'paper' => array( 0, 0, $paper_width_pt, 14000.0 ), |
| 65 |
'default_font' => 'dejavu sans mono', |
| 66 |
'fit_height' => true, |
| 67 |
'receipt_layout' => true, |
| 68 |
) |
| 69 |
); |
| 70 |
} |
| 71 |
|
| 72 |
$html = $this->render_html_engine( $engine, $template, $order ); |
| 73 |
|
| 74 |
// Non-thermal templates render to a standard A4 portrait page (Pdf_Renderer default). |
| 75 |
return ( new Pdf_Renderer() )->render_html( $html, array( 'receipt_layout' => true ) ); |
| 76 |
} |
| 77 |
|
| 78 |
/** |
| 79 |
* Resolve a thermal template's paper width in points. |
| 80 |
* |
| 81 |
* Template metadata declares the physical roll ('58mm' / '80mm'); fall back |
| 82 |
* to 80mm when absent so templates without metadata keep the previous page |
| 83 |
* size. |
| 84 |
* |
| 85 |
* @param array $template Template metadata/content. |
| 86 |
* |
| 87 |
* @return float Paper width in pt. |
| 88 |
*/ |
| 89 |
private function thermal_paper_width_pt( array $template ): float { |
| 90 |
$raw = isset( $template['paper_width'] ) ? (string) $template['paper_width'] : ''; |
| 91 |
$mm = (float) $raw; // Leading-number cast: '58mm' → 58.0. |
| 92 |
|
| 93 |
if ( $mm < 25.0 || $mm > 250.0 ) { |
| 94 |
$mm = 80.0; |
| 95 |
} |
| 96 |
|
| 97 |
return round( $mm * 72 / 25.4, 2 ); |
| 98 |
} |
| 99 |
|
| 100 |
/** |
| 101 |
* Render WP Overnight integration templates using the plugin's native PDF bytes. |
| 102 |
* |
| 103 |
* @param array $template Template metadata/content. |
| 104 |
* @param WC_Abstract_Order $order The order to render. |
| 105 |
* |
| 106 |
* @return string|null Native PDF bytes for WP Overnight templates, null for all other templates. |
| 107 |
* @throws \RuntimeException When the WP Overnight PDF document cannot be generated. |
| 108 |
*/ |
| 109 |
private function maybe_render_wp_overnight_pdf( array $template, WC_Abstract_Order $order ): ?string { |
| 110 |
$document_type = $this->wp_overnight_document_type( $template ); |
| 111 |
if ( null === $document_type ) { |
| 112 |
return null; |
| 113 |
} |
| 114 |
|
| 115 |
$document = apply_filters( 'woocommerce_pos_wp_overnight_pdf_document', null, $document_type, $order ); |
| 116 |
if ( null === $document && \function_exists( 'wcpdf_get_document' ) ) { |
| 117 |
$document = wcpdf_get_document( $document_type, $order, true ); |
| 118 |
} |
| 119 |
|
| 120 |
if ( ! $document || ! \is_callable( array( $document, 'get_pdf' ) ) ) { |
| 121 |
throw new \RuntimeException( |
| 122 |
esc_html( |
| 123 |
sprintf( |
| 124 |
/* translators: %s: WP Overnight document type. */ |
| 125 |
__( 'WP Overnight %s PDF could not be generated.', 'woocommerce-pos' ), |
| 126 |
$document_type |
| 127 |
) |
| 128 |
) |
| 129 |
); |
| 130 |
} |
| 131 |
|
| 132 |
$pdf = $document->get_pdf(); |
| 133 |
if ( ! \is_string( $pdf ) || '' === $pdf || 0 !== strpos( $pdf, '%PDF-' ) ) { |
| 134 |
throw new \RuntimeException( |
| 135 |
esc_html( |
| 136 |
sprintf( |
| 137 |
/* translators: %s: WP Overnight document type. */ |
| 138 |
__( 'WP Overnight %s PDF could not be generated.', 'woocommerce-pos' ), |
| 139 |
$document_type |
| 140 |
) |
| 141 |
) |
| 142 |
); |
| 143 |
} |
| 144 |
|
| 145 |
return $pdf; |
| 146 |
} |
| 147 |
|
| 148 |
/** |
| 149 |
* Map WCPOS virtual template IDs to WP Overnight document types. |
| 150 |
* |
| 151 |
* @param array $template Template metadata/content. |
| 152 |
* |
| 153 |
* @return string|null invoice|packing-slip for WP Overnight templates, null otherwise. |
| 154 |
*/ |
| 155 |
private function wp_overnight_document_type( array $template ): ?string { |
| 156 |
$id = isset( $template['id'] ) ? (string) $template['id'] : ''; |
| 157 |
|
| 158 |
if ( 'wp-overnight-invoice' === $id ) { |
| 159 |
return 'invoice'; |
| 160 |
} |
| 161 |
|
| 162 |
if ( 'wp-overnight-packing-slip' === $id ) { |
| 163 |
return 'packing-slip'; |
| 164 |
} |
| 165 |
|
| 166 |
return null; |
| 167 |
} |
| 168 |
|
| 169 |
/** |
| 170 |
* Run an echo-based renderer (logicless / legacy-php) and capture its HTML. |
| 171 |
* |
| 172 |
* @param string $engine The template engine. |
| 173 |
* @param array $template Template metadata/content. |
| 174 |
* @param WC_Abstract_Order $order The order to render. |
| 175 |
* |
| 176 |
* @return string The captured receipt HTML. |
| 177 |
*/ |
| 178 |
private function render_html_engine( string $engine, array $template, WC_Abstract_Order $order ): string { |
| 179 |
$receipt_data = ( new Receipt_Data_Builder() )->build( $order, 'live' ); |
| 180 |
$renderer = ( new Receipt_Renderer_Factory() )->create( $engine ); |
| 181 |
|
| 182 |
ob_start(); |
| 183 |
try { |
| 184 |
$renderer->render( $template, $order, $receipt_data ); |
| 185 |
} finally { |
| 186 |
$html = ob_get_clean(); |
| 187 |
} |
| 188 |
|
| 189 |
return false === $html ? '' : $html; |
| 190 |
} |
| 191 |
} |
| 192 |
|