0.62; $on = $light ? '#18213a' : '#ffffff'; $soft = $light ? 'rgba(24, 33, 58, 0.72)' : 'rgba(255, 255, 255, 0.82)'; $split = $light ? 'rgba(24, 33, 58, 0.16)' : 'rgba(255, 255, 255, 0.24)'; return ".agency { color: $color; }\n" . ".rule { background: $color; }\n" . ".band { background: $color; color: $on; }\n" . ".band td + td { border-left: 1px solid $split; }\n" . ".band .n span { color: $soft; }\n" . ".band .s { color: $soft; }\n" . ".bar div { background: $color; }\n"; } /** * The report as PDF bytes. * * @param array $report From Monthwrap_Report::build(). * @return string */ public static function pdf( array $report ) { try { return self::render( $report ); } catch ( Throwable $e ) { if ( empty( $report['settings']['logo_id'] ) ) { throw $e; } // A logo the server can't process must never stop the report: make it without. $report['settings']['logo_id'] = 0; return self::render( $report ); } } /** * Runs Dompdf. * * @param array $report From Monthwrap_Report::build(). * @return string */ private static function render( array $report ) { require_once MONTHWRAP_DIR . 'vendor/autoload.php'; $temp = trailingslashit( get_temp_dir() ); $options = new \Dompdf\Options(); $options->setIsRemoteEnabled( false ); $options->setIsPhpEnabled( false ); $options->setIsJavascriptEnabled( false ); $options->setDefaultFont( 'DejaVu Sans' ); $options->setDefaultMediaType( 'print' ); $options->setChroot( array( MONTHWRAP_DIR ) ); $options->setTempDir( $temp ); $options->setFontCache( $temp ); $dompdf = new \Dompdf\Dompdf( $options ); $dompdf->loadHtml( self::document( $report, false ), 'UTF-8' ); $css = $dompdf->getCss(); $brand = self::brand_css( $report ); // Dompdf's own styles go first and load only once: read later, their page margin wins. $css->load_css_file( $css->getDefaultStylesheet(), \Dompdf\Css\Stylesheet::ORIG_UA ); $css->load_css_file( MONTHWRAP_DIR . 'assets/report.css' ); $css->load_css( $brand ); $dompdf->setPaper( self::paper(), 'portrait' ); $dompdf->render(); return (string) $dompdf->output(); } /** * A file name like "mikes-plumbing-website-report-2026-10.pdf". * * @param array $report From Monthwrap_Report::build(). * @return string */ public static function filename( array $report ) { $site = sanitize_title( $report['site']['name'] ); return ( '' !== $site ? $site . '-' : '' ) . 'website-report-' . $report['period']['start']->format( 'Y-m' ) . '.pdf'; } /** * Letter paper in the US and Canada, A4 everywhere else. * * @return string */ private static function paper() { return in_array( get_locale(), array( 'en_US', 'en_CA', 'es_MX', 'fr_CA' ), true ) ? 'letter' : 'a4'; } /** * The agency logo from the media library, embedded as a data: URI. * * PNG, JPEG or GIF up to 5 MB. It's flattened onto white (the page is white) and made at * most 600 pixels wide with PHP's GD, then embedded as a PNG without transparency. * Transparent PNGs would otherwise go through ImageMagick, which on many hosts can't read * PNGs and stops the PDF. * Without GD, or for anything else, the logo is left out and the agency name shows. * * @param int $attachment_id Attachment ID. * @return string */ public static function logo( $attachment_id ) { if ( ! $attachment_id || ! function_exists( 'imagecreatefromstring' ) ) { return ''; } $path = get_attached_file( $attachment_id ); $type = get_post_mime_type( $attachment_id ); if ( ! $path || ! in_array( $type, array( 'image/png', 'image/jpeg', 'image/gif' ), true ) || ! is_readable( $path ) || filesize( $path ) > 5 * MB_IN_BYTES ) { return ''; } global $wp_filesystem; if ( ! function_exists( 'WP_Filesystem' ) ) { require_once ABSPATH . 'wp-admin/includes/file.php'; } if ( ! WP_Filesystem() || ! $wp_filesystem ) { return ''; } $source = @imagecreatefromstring( (string) $wp_filesystem->get_contents( $path ) ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- a broken image just means no logo. if ( ! $source ) { return ''; } $width = imagesx( $source ); $height = imagesy( $source ); $scale = min( 1, 600 / max( 1, $width ) ); $canvas = imagecreatetruecolor( max( 1, (int) round( $width * $scale ) ), max( 1, (int) round( $height * $scale ) ) ); imagefill( $canvas, 0, 0, imagecolorallocate( $canvas, 255, 255, 255 ) ); imagecopyresampled( $canvas, $source, 0, 0, 0, 0, imagesx( $canvas ), imagesy( $canvas ), $width, $height ); imagesavealpha( $canvas, false ); ob_start(); imagepng( $canvas, null, 9 ); $png = (string) ob_get_clean(); imagedestroy( $source ); imagedestroy( $canvas ); return '' !== $png ? 'data:image/png;base64,' . base64_encode( $png ) : ''; // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- embedding the logo image in the PDF, not hiding code. } }