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.
}
}