loadHtml( $html, 'UTF-8' ); $dompdf->setPaper( self::paperSize(), 'portrait' ); $dompdf->render(); $output = $dompdf->output(); if ( ! is_string( $output ) || strncmp( $output, '%PDF-', 5 ) !== 0 ) { return new \WP_Error( 'easy_invoice_pdf_bad_output', __( 'The PDF library returned an unreadable file.', 'easy-invoice' ) ); } return $output; } catch ( \Throwable $e ) { // A malformed template or an unreachable asset should degrade to the // browser path, never take down the request that asked for the PDF. error_log( 'Easy Invoice: PDF rendering failed — ' . $e->getMessage() ); return new \WP_Error( 'easy_invoice_pdf_failed', __( 'The PDF could not be generated.', 'easy-invoice' ) ); } } /** * Render the PDF template to an HTML string. * * @param object $document Invoice or Quote model. * @param string $type 'invoice' or 'quote'. * @return string|\WP_Error */ private static function buildHtml( $document, string $type ) { /** * Fires before a document's PDF markup is built, whichever layout is * used. Pro's Client Language switches locale here. * * @param object $document Invoice or Quote model. * @param string $type 'invoice' or 'quote'. */ do_action( 'easy_invoice_pdf_before_build', $document, $type ); // The chosen design first, unless the site asked for the plain layout // or the design cannot be rendered (an empty Pro canvas, say). if ( self::useDesign( $document, $type ) ) { $design = self::buildDesignHtml( $document, $type ); if ( is_string( $design ) && '' !== $design ) { return $design; } } $default = easy_invoice_locate_template( 'pdf/' . $type . '.php', [ 'type' => $type ] ); /** * Filter the template used for server-side PDF output. * * @param string $default Absolute path to the template. * @param object $document Invoice or Quote model. * @param string $type 'invoice' or 'quote'. */ $template = (string) apply_filters( self::TEMPLATE_FILTER, $default, $document, $type ); if ( ! $template || ! file_exists( $template ) ) { return new \WP_Error( 'easy_invoice_pdf_no_template', __( 'The PDF template is missing.', 'easy-invoice' ) ); } // Exposed to the template. Named to match the on-screen designs so the two // stay recognisably related to anyone editing both. $invoice = $document; // phpcs:ignore -- consumed by the template. $formatter = self::formatterFor( $document ); ob_start(); include $template; $html = (string) ob_get_clean(); return $html !== '' ? $html : new \WP_Error( 'easy_invoice_pdf_empty', __( 'The PDF template produced no output.', 'easy-invoice' ) ); } /** * Whether the PDF should reproduce the design chosen for the document. * * @param object $document Invoice or Quote model. * @param string $type 'invoice' or 'quote'. * @return bool */ public static function useDesign( $document, string $type ): bool { $mode = (string) get_option( 'easy_invoice_pdf_layout', 'design' ); /** * Filter whether the PDF reproduces the on-screen design ('design') or * uses the plain print layout ('plain'). * * @param bool $use True to render the selected design. * @param object $document Invoice or Quote model. * @param string $type 'invoice' or 'quote'. */ return (bool) apply_filters( 'easy_invoice_pdf_use_design', 'plain' !== $mode, $document, $type ); } /** * Render the document's on-screen design for dompdf. * * The design templates are the same files the public page includes; what * differs is the frame: no page chrome, a print stylesheet that maps their * flex/grid layout onto tables, and custom properties resolved to values. * Everything the plain PDF adds after the document — attachments, the * signature block, e-invoice notes — is fired here as well. * * @param object $document Invoice or Quote model. * @param string $type 'invoice' or 'quote'. * @return string HTML, or '' when the design produced nothing usable. */ private static function buildDesignHtml( $document, string $type ): string { $is_quote = ( 'quote' === $type ); $design = is_callable( [ $document, 'getTemplate' ] ) ? (string) $document->getTemplate() : ''; $file = function_exists( 'easy_invoice_design_template' ) ? easy_invoice_design_template( $type, $design ) : ''; if ( '' === $file || ! file_exists( $file ) ) { return ''; } // The variables the designs read; identical to templates/document/single.php. $invoice = $is_quote ? null : $document; // phpcs:ignore -- consumed by the template. $quote = $is_quote ? $document : null; // phpcs:ignore -- consumed by the template. $formatter = self::formatterFor( $document ); // phpcs:ignore -- consumed by the template. $text_settings = $is_quote // phpcs:ignore -- consumed by the template. ? \EasyInvoice\Helpers\TemplateTextHelper::getQuoteTextSettings() : \EasyInvoice\Helpers\TemplateTextHelper::getInvoiceTextSettings(); $company_info = \EasyInvoice\Helpers\TemplateTextHelper::getCompanyInfo(); // phpcs:ignore -- consumed by the template. $ei_pdf_type = $type; // phpcs:ignore -- consumed by hooks. // Some design hooks (Pro's Template Builder among them) read the // document from the global post, as they would on the public page. // Emails and cron have no such post, so stand it up for the render. global $post; $previous_post = $post; $document_post = get_post( (int) $document->getId() ); if ( $document_post instanceof \WP_Post ) { $post = $document_post; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- restored below. setup_postdata( $post ); } self::$rendering = true; ob_start(); try { include $file; } catch ( \Throwable $e ) { ob_end_clean(); self::$rendering = false; $post = $previous_post; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited error_log( 'Easy Invoice: design PDF failed, using the plain layout — ' . $e->getMessage() ); return ''; } $body = (string) ob_get_clean(); self::$rendering = false; $post = $previous_post; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited if ( $previous_post instanceof \WP_Post ) { setup_postdata( $previous_post ); } // A design that did not draw the line-item table (the hook-driven // "default" canvas with nothing registered on it) is not a document. $has_items = false !== strpos( $body, $is_quote ? 'quote-items' : 'invoice-items' ); $has_canvas = false !== strpos( $body, 'canvas-element' ); // Pro Template Builder output. if ( ! $has_items && ! $has_canvas ) { return ''; } // Payment position, unless the design already prints one. $extra = ''; if ( ! $is_quote && false === strpos( $body, 'partial-payments-breakdown' ) && false === strpos( $body, 'invoice-balance-due' ) ) { $invoice_id = (int) $document->getId(); $paid = self::amountPaid( $invoice_id ); $credited = InvoiceBalance::credited( $invoice_id ); if ( $paid > 0 || $credited > 0 ) { $total = is_callable( [ $document, 'getTotal' ] ) ? (float) $document->getTotal() : 0.0; $extra .= '
| ' . esc_html( sprintf( __( 'Credit note %s', 'easy-invoice' ), (string) get_post_meta( $credit_id, '_easy_invoice_number', true ) ) ) . ' | -' . esc_html( $formatter->format( $credit_amount ) ) . ' |
| ' . esc_html__( 'Paid', 'easy-invoice' ) . ' | -' . esc_html( $formatter->format( $paid ) ) . ' |
| ' . esc_html__( 'Balance due', 'easy-invoice' ) . ' | ' . esc_html( $formatter->format( max( 0, $total - $paid - $credited ) ) ) . ' |