its counts and when it was measured. */ public static function index(): array { $i = get_option( self::INDEX, array() ); return is_array( $i ) ? $i : array(); } /** * One measured page's record. * * @param string $page The page's address. * @return array The record, or an empty array. */ public static function record_for( string $page ): array { $r = get_option( self::page_option( $page ), array() ); return is_array( $r ) && ( $r['url'] ?? '' ) === $page ? $r : array(); } /* ---- From samples to rules (never smaller than measured) ---- */ /** * Samples: [viewport width, picture width] pairs, viewport ascending. Returns rules [[max viewport or 0 for the last, * share of the viewport, pixels], ...] whose value is at or above every sample, at most MAX_RULES. * * @param array $samples Pairs of viewport width and picture width. * @return array The rules. */ public static function rules( array $samples ): array { $pts = array(); foreach ( $samples as $s ) { if ( ! is_array( $s ) || count( $s ) < 2 || ! is_numeric( $s[0] ) || ! is_numeric( $s[1] ) ) { return array(); } $v = (int) $s[0]; $w = (float) $s[1]; if ( $v < 200 || $v > 4000 || $w < 0 || $w > 8000 || ( $pts && $v <= end( $pts )[0] ) ) { return array(); } $pts[] = array( $v, $w ); } // A picture hidden at some widths downloads nothing useful there: those widths take its smallest visible width. $visible = array_values( array_filter( array_column( $pts, 1 ), static fn( $w ) => $w >= 1 ) ); if ( count( $pts ) < 2 || ! $visible ) { return array(); } $floor = (float) min( $visible ); foreach ( $pts as &$p ) { $p[1] = (float) ceil( $p[1] >= 1 ? $p[1] : $floor ); } unset( $p ); foreach ( array( 1.0, 2.0, 4.0, 8.0, 16.0, 32.0, 64.0 ) as $tolerance ) { $rules = self::segments( $pts, $tolerance ); if ( count( $rules ) <= self::MAX_RULES ) { return $rules; } } // Still too many: one width, the largest measured, for every screen. return array( array( 0, 0.0, (float) max( array_column( $pts, 1 ) ) ) ); } /** * Straight-line rules over the samples, each as close above them as the tolerance allows. * * @param array $pts Viewport and width points. * @param float $tolerance How far above a sample a rule may run. * @return array The rules. */ private static function segments( array $pts, float $tolerance ): array { $rules = array(); $n = count( $pts ); $i = 0; while ( $i < $n ) { $j = $i; $best = array( 0.0, $pts[ $i ][1] ); for ( $k = $i + 1; $k < $n; $k++ ) { $a = ( $pts[ $k ][1] - $pts[ $i ][1] ) / ( $pts[ $k ][0] - $pts[ $i ][0] ); $b = -INF; $ok = true; for ( $m = $i; $m <= $k; $m++ ) { $b = max( $b, $pts[ $m ][1] - $a * $pts[ $m ][0] ); } for ( $m = $i; $m <= $k; $m++ ) { if ( $a * $pts[ $m ][0] + $b - $pts[ $m ][1] > $tolerance ) { $ok = false; break; } } if ( ! $ok ) { break; } $j = $k; $best = array( $a, $b ); } if ( abs( $best[0] ) < 0.00001 ) { $best = array( 0.0, (float) max( array_column( array_slice( $pts, $i, $j - $i + 1 ), 1 ) ) ); } $rules[] = array( $j === $n - 1 ? 0 : $pts[ $j ][0], round( $best[0], 6 ), ceil( ( $best[1] + 0.5 ) * 100 ) / 100 ); $i = $j + 1; } return $rules; } /** * The width the rules give at a viewport width. * * @param array $rules The rules. * @param int $viewport The viewport width. * @return float The width in pixels, at least 1. */ public static function width_at( array $rules, int $viewport ): float { foreach ( $rules as $r ) { if ( 0 === (int) $r[0] || $viewport <= (int) $r[0] ) { return max( 1.0, (float) $r[1] * $viewport + (float) $r[2] ); } } return 1.0; } /** * The sizes attribute of the rules. * * @param array $rules The rules. * @return string The sizes attribute. */ public static function sizes( array $rules ): string { $parts = array(); foreach ( $rules as $r ) { $a = (float) $r[1]; $b = (float) $r[2]; if ( abs( $a ) < 0.00001 ) { $value = self::number( max( 1.0, $b ) ) . 'px'; } else { $value = 'calc(' . self::number( $a * 100 ) . 'vw ' . ( $b < 0 ? '- ' : '+ ' ) . self::number( abs( $b ) ) . 'px)'; } $parts[] = 0 === (int) $r[0] ? $value : '(max-width: ' . (int) $r[0] . 'px) ' . $value; } return implode( ', ', $parts ); } /** * A number with at most four decimals and no trailing zeros, for a sizes attribute. * * @param float $n The number. * @return string The number as text. */ private static function number( float $n ): string { return rtrim( rtrim( number_format( $n, 4, '.', '' ), '0' ), '.' ); } /** * Pixel widths the measured slots need on the DEVICES screens. * * @param array $rules The rules. * @return array The widths, smallest first. */ public static function needs( array $rules ): array { $out = array(); foreach ( self::DEVICES as list( $viewport, $density ) ) { $out[] = (int) ceil( self::width_at( $rules, $viewport ) * $density ); } $out = array_values( array_unique( $out ) ); sort( $out ); return $out; } /* ---- Saving one measured page (the admin's browser posts it; Admin::perform('fit_result')) ---- */ /** * $images: [{src, samples: [[viewport, width], ...], sizes (the picture's own sizes attribute), srcset (bool), * art (picture with media sources), gallery (WooCommerce gallery picture)}]. Returns the page's counts. */ /** * The local file of a picture address on this site (the uploads folder or the active theme), or ''. * * @param string $src The picture's address as the page has it. * @param string $page The page. * @return string */ private static function local_file( string $src, string $page ): string { $url = Urls::absolute( html_entity_decode( $src, ENT_QUOTES | ENT_HTML5, 'UTF-8' ), $page ); $p = wp_parse_url( $url ); if ( ! is_array( $p ) || empty( $p['path'] ) || ! Urls::safe_path( (string) $p['path'] ) ) { return ''; } $up = wp_get_upload_dir(); $roots = array( $up['baseurl'] => $up['basedir'], get_stylesheet_directory_uri() => get_stylesheet_directory(), get_template_directory_uri() => get_template_directory(), ); foreach ( $roots as $base => $dir ) { $b = wp_parse_url( $base ); if ( ! is_array( $b ) || Urls::origin( $url ) !== Urls::origin( $base ) ) { continue; } $prefix = trailingslashit( (string) ( $b['path'] ?? '/' ) ); if ( str_starts_with( (string) $p['path'], $prefix ) ) { $relative = rawurldecode( substr( (string) $p['path'], strlen( $prefix ) ) ); if ( '' !== $relative && ! str_contains( $relative, '..' ) ) { return trailingslashit( $dir ) . $relative; } } } return ''; } /** * A measured page, its pictures' widths and (1.9.0) the pictures without a width and height the measurement showed * can get their file's size: each one is kept only when its size is the size of the real file. * * @param string $url The page. * @param array $images The measured pictures. * @param array $dims Pictures without a width and height: src, width, height, safe. * @return array Counts: images, fitted, theme, skipped, sized. * @throws \RuntimeException When the page cannot be measured. */ public static function save_page( string $url, array $images, array $dims = array() ): array { $held = Reviewed_Store::enter( 8 ); try { if ( ! current_user_can( 'manage_options' ) || ! current_user_can( 'upload_files' ) ) { throw new \RuntimeException( 'image_access_denied' ); } $page = Urls::page( $url ); if ( is_wp_error( $page ) ) { throw new \RuntimeException( 'fit_page_not_public' ); } $index = self::index(); if ( ! isset( $index[ $page ] ) && count( $index ) >= self::MAX_PAGES ) { throw new \RuntimeException( 'fit_page_limit' ); } // As the scan does for a public page: index the page's own pictures (nothing is processed), so a picture of // a page measured before any scan keeps its measured widths. $post_id = url_to_postid( $page ); if ( $post_id && Sources::public_post( $post_id ) ) { Sources::post( $post_id ); } $record = array( 'url' => $page, 'time' => time(), 'images' => array(), ); $counts = array( 'images' => 0, 'fitted' => 0, 'theme' => 0, 'skipped' => 0, 'sized' => 0, ); $needs = array(); foreach ( array_slice( $images, 0, self::MAX_IMAGES ) as $img ) { if ( ! is_array( $img ) || ! is_string( $img['src'] ?? null ) || ! is_array( $img['samples'] ?? null ) || count( $img['samples'] ) > self::MAX_SAMPLES ) { continue; } $key = self::key( $img['src'], $page ); if ( '' === $key || isset( $record['images'][ $key ] ) ) { continue; } ++$counts['images']; $rules = self::rules( $img['samples'] ); if ( ! $rules || ! empty( $img['art'] ) || ! empty( $img['gallery'] ) || empty( $img['srcset'] ) ) { ++$counts['skipped']; continue; } if ( ! self::is_default( is_string( $img['sizes'] ?? null ) ? $img['sizes'] : '' ) ) { ++$counts['theme']; continue; } $record['images'][ $key ] = array( 'sizes' => self::sizes( $rules ), 'rules' => $rules, ); ++$counts['fitted']; // The widths this page needs, for the source of this picture (Media Library or active theme). $m = Sources::map( Urls::absolute( $img['src'], $page ), 0, true ); $row = $m ? Repository::attachment( (int) $m['id'] ) : Theme::find( Urls::absolute( $img['src'], $page ) ); if ( $row ) { $needs[ (int) $row['id'] ] = array_merge( $needs[ (int) $row['id'] ] ?? array(), self::needs( $rules ) ); } } foreach ( array_slice( $dims, 0, self::MAX_IMAGES ) as $d ) { if ( ! is_array( $d ) || true !== ( $d['safe'] ?? null ) || ! is_string( $d['src'] ?? null ) || ! is_int( $d['width'] ?? null ) || ! is_int( $d['height'] ?? null ) || $d['width'] < 1 || $d['height'] < 1 || $d['width'] > 20000 || $d['height'] > 20000 ) { continue; } $key = self::key( $d['src'], $page ); $file = '' === $key ? '' : self::local_file( $d['src'], $page ); $size = '' !== $file && is_file( $file ) ? wp_getimagesize( $file ) : false; if ( ! is_array( $size ) || (int) $size[0] !== $d['width'] || (int) $size[1] !== $d['height'] || isset( $record['dims'][ $key ] ) ) { continue; } $record['dims'][ $key ] = array( $d['width'], $d['height'] ); ++$counts['sized']; } Reviewed_Store::update_option( self::page_option( $page ), $record, false ); $index[ $page ] = array_merge( $counts, array( 'time' => time() ) ); Reviewed_Store::update_option( self::INDEX, $index, false ); foreach ( $needs as $source => $widths ) { self::add_needs( (int) $source, $page, $widths ); } return $counts; } finally { Reviewed_Store::leave( $held ); } } /** * A source keeps the needs of each measured page (at most twelve pages), and their union for the size plan. * * @param int $source The source row. * @param string $page The measured page. * @param array $widths The widths the page's slots need. */ private static function add_needs( int $source, string $page, array $widths ): void { $row = Repository::row( $source ); if ( ! $row ) { return; } $d = $row['details']; $per = is_array( $d['fit_pages'] ?? null ) ? $d['fit_pages'] : array(); $widths = array_values( array_unique( array_map( 'intval', $widths ) ) ); sort( $widths ); $per[ substr( hash( 'sha256', $page ), 0, 16 ) ] = array_slice( $widths, 0, 32 ); $per = array_slice( $per, -12, null, true ); $all = array(); foreach ( $per as $list ) { $all = array_merge( $all, $list ); } $all = array_values( array_unique( $all ) ); sort( $all ); $d['fit_pages'] = $per; $d['fit_need'] = array_slice( $all, 0, 64 ); $d['fit_time'] = time(); Repository::update( $source, array( 'details' => $d ) ); } /** Measured sources (for "Make the sizes these pages need"). */ public static function measured_sources(): array { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Image Manager's own index rows that carry measured widths; read for one explicit bulk selection. $ids = $wpdb->get_col( $wpdb->prepare( "SELECT id FROM %i WHERE details LIKE %s AND state<>'grouped' AND excluded=0 ORDER BY id ASC LIMIT 1000", Repository::table( 'sources' ), '%' . $wpdb->esc_like( '"fit_need"' ) . '%' ) ); return array_map( 'intval', is_array( $ids ) ? $ids : array() ); } /** * Forget every measured page and the widths sources kept from them. * * @return int How many pages were forgotten. */ public static function forget(): int { $held = Reviewed_Store::enter( 8 ); try { $index = self::index(); foreach ( array_keys( $index ) as $page ) { Reviewed_Store::delete_option( self::page_option( (string) $page ) ); } Reviewed_Store::delete_option( self::INDEX ); foreach ( self::measured_sources() as $id ) { $row = Repository::row( $id ); if ( ! $row ) { continue; } $d = $row['details']; unset( $d['fit_pages'], $d['fit_need'], $d['fit_time'] ); Repository::update( $id, array( 'details' => $d ) ); } return count( $index ); } finally { Reviewed_Store::leave( $held ); } } /** Pages to measure: Home and the public pages Image Manager's scan found. */ public static function pages(): array { $out = array(); $home = Urls::page( home_url( '/' ) ); if ( ! is_wp_error( $home ) ) { $out[] = $home; } foreach ( Scanner::pages( self::MAX_PAGES ) as $p ) { if ( 'scanned' !== ( $p['state'] ?? '' ) ) { continue; } $u = Urls::page( (string) $p['url'] ); if ( ! is_wp_error( $u ) ) { $out[] = $u; } } /** * Filters the pages Image Manager measures for Pictures sized to fit the page: Home and the public pages its scan * found. Every address must still be one of this site's public pages (the same rule as the scan); others are dropped. * * @since 1.8.0 * * @param string[] $pages Page addresses. */ $pages = apply_filters( 'afsd_im_fit_pages', $out ); $clean = array(); foreach ( is_array( $pages ) ? $pages : $out as $url ) { if ( ! is_string( $url ) ) { continue; } $u = Urls::page( $url ); if ( ! is_wp_error( $u ) ) { $clean[] = $u; } } return array_slice( array_values( array_unique( $clean ) ), 0, self::MAX_PAGES ); } /* ---- The page a visitor gets ---- */ /** * The pass on a measured public page, where delivery allows fitted sizes or dimensions: a pass on the page's whole * HTML through WordPress's template enhancement output buffer (Hooks::PAGE_HTML). */ public static function start_buffer(): void { $fit = Delivery::allowed( 'fit' ); $dims = Delivery::allowed( 'dimensions' ); if ( ! $fit && ! $dims ) { return; } $page = self::current_page(); if ( '' === $page ) { return; } $record = self::record_for( $page ); $fit = $fit && ! empty( $record['images'] ); $dims = $dims && ! empty( $record['dims'] ); if ( ! $fit && ! $dims ) { return; } add_filter( Hooks::PAGE_HTML, static function ( string $html ) use ( $record, $page, $fit, $dims ): string { try { $html = $fit ? self::apply( $html, $record['images'], $page ) : $html; return $dims ? self::apply_dims( $html, $record['dims'], $page ) : $html; } catch ( \Throwable $e ) { return $html; } // An optional attribute never breaks a page. }, Hooks::PASS_FIT ); } /** * 1.9.0: one bounded pass that gives a measured picture without a width and height its file's size (pictures in a * picture element, with a srcset, or inside noscript, template or svg stay as they are). * * @param string $html The page. * @param array $dims Address path => [width, height], from the page's measurement. * @param string $page The page. * @return string */ public static function apply_dims( string $html, array $dims, string $page ): string { if ( '' === $html || strlen( $html ) > 2097152 || ! class_exists( '\WP_HTML_Tag_Processor' ) ) { return $html; } $p = new \WP_HTML_Tag_Processor( $html ); if ( ! method_exists( $p, 'set_attribute' ) || ! method_exists( $p, 'is_tag_closer' ) ) { return $html; } $skip = 0; $in_picture = false; $n = 0; while ( $n++ < 20000 && $p->next_tag( array( 'tag_closers' => 'visit' ) ) ) { $tag = $p->get_tag(); $close = $p->is_tag_closer(); if ( in_array( $tag, array( 'NOSCRIPT', 'TEMPLATE', 'SVG' ), true ) ) { $skip = $close ? max( 0, $skip - 1 ) : $skip + 1; continue; } if ( 'PICTURE' === $tag ) { $in_picture = ! $close; continue; } if ( $skip || $close || $in_picture || 'IMG' !== $tag || null !== $p->get_attribute( 'width' ) || null !== $p->get_attribute( 'height' ) || null !== $p->get_attribute( 'srcset' ) ) { continue; } $src = $p->get_attribute( 'src' ); $size = is_string( $src ) ? ( $dims[ self::key( $src, $page ) ] ?? null ) : null; if ( is_array( $size ) && 2 === count( $size ) ) { $p->set_attribute( 'width', (string) (int) $size[0] ); $p->set_attribute( 'height', (string) (int) $size[1] ); } } return $p->get_updated_html(); } /** * This request's page address, when it is a public page Image Manager may read. * * @return string The address, or an empty string. */ private static function current_page(): string { $uri = isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : '/'; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- validated by the strict URL policy of Urls::page(). $home = wp_parse_url( home_url( '/' ) ); $page = Urls::page( ( $home['scheme'] ?? 'https' ) . '://' . ( $home['host'] ?? '' ) . ( isset( $home['port'] ) ? ':' . $home['port'] : '' ) . $uri ); return is_wp_error( $page ) ? '' : $page; } /** * Two bounded attribute passes: find the pictures to change, then change them (and their sources). * * @param string $html The page. * @param array $images The page record's pictures. * @param string $page The page's address. * @return string The page with the measured sizes, and width and height where measured safe. */ public static function apply( string $html, array $images, string $page ): string { if ( '' === $html || strlen( $html ) > 2097152 || ! class_exists( '\WP_HTML_Tag_Processor' ) ) { return $html; } $p = new \WP_HTML_Tag_Processor( $html ); if ( ! method_exists( $p, 'set_attribute' ) || ! method_exists( $p, 'is_tag_closer' ) ) { return $html; } $skip = 0; $picture = -1; $pictures = 0; $in_picture = false; $art = false; $img = 0; $plan_img = array(); $plan_picture = array(); $n = 0; while ( $n++ < 20000 && $p->next_tag( array( 'tag_closers' => 'visit' ) ) ) { $tag = $p->get_tag(); $close = $p->is_tag_closer(); if ( in_array( $tag, array( 'NOSCRIPT', 'TEMPLATE', 'SVG' ), true ) ) { $skip = $close ? max( 0, $skip - 1 ) : $skip + 1; continue; } if ( 'PICTURE' === $tag ) { if ( $close ) { $in_picture = false; } else { $in_picture = true; $picture = $pictures++; $art = false; } continue; } if ( $skip || $close ) { continue; } if ( 'SOURCE' === $tag && $in_picture && null !== $p->get_attribute( 'media' ) ) { $art = true; continue; } if ( 'IMG' !== $tag ) { continue; } $index = $img++; if ( $in_picture && $art ) { continue; } $src = $p->get_attribute( 'src' ); $sizes = $p->get_attribute( 'sizes' ); if ( ! is_string( $src ) || null !== $p->get_attribute( 'data-large_image' ) || ! self::is_default( is_string( $sizes ) ? $sizes : null ) ) { continue; } $measured = $images[ self::key( $src, $page ) ]['sizes'] ?? ''; if ( ! is_string( $measured ) || '' === $measured ) { continue; } // A srcset is what the browser chooses from; without one (and no sources), sizes changes nothing. if ( ! is_string( $p->get_attribute( 'srcset' ) ) && ! $in_picture ) { continue; } $auto = is_string( $sizes ) && preg_match( '/^\s*auto\s*(?:,|$)/i', $sizes ) && 'lazy' === strtolower( (string) $p->get_attribute( 'loading' ) ); $plan_img[ $index ] = ( $auto ? 'auto, ' : '' ) . $measured; if ( $in_picture ) { $plan_picture[ $picture ] = $measured; } } if ( ! $plan_img ) { return $html; } $p = new \WP_HTML_Tag_Processor( $html ); $skip = 0; $picture = -1; $pictures = 0; $in_picture = false; $img = 0; $n = 0; while ( $n++ < 20000 && $p->next_tag( array( 'tag_closers' => 'visit' ) ) ) { $tag = $p->get_tag(); $close = $p->is_tag_closer(); if ( in_array( $tag, array( 'NOSCRIPT', 'TEMPLATE', 'SVG' ), true ) ) { $skip = $close ? max( 0, $skip - 1 ) : $skip + 1; continue; } if ( 'PICTURE' === $tag ) { if ( $close ) { $in_picture = false; } else { $in_picture = true; $picture = $pictures++; } continue; } if ( $skip || $close ) { continue; } if ( 'SOURCE' === $tag && $in_picture && isset( $plan_picture[ $picture ] ) && is_string( $p->get_attribute( 'srcset' ) ) && self::is_default( $p->get_attribute( 'sizes' ) ) ) { $p->set_attribute( 'sizes', $plan_picture[ $picture ] ); continue; } if ( 'IMG' === $tag ) { $index = $img++; if ( isset( $plan_img[ $index ] ) ) { $p->set_attribute( 'sizes', $plan_img[ $index ] ); } } } return $p->get_updated_html(); } /** The report of the Optimize tab: pages measured, pictures fitted and those the theme sizes itself. */ public static function report(): array { $index = self::index(); $out = array( 'pages' => count( $index ), 'fitted' => 0, 'theme' => 0, 'skipped' => 0, 'images' => 0, 'sized' => 0, 'last' => 0, 'list' => array(), ); foreach ( $index as $page => $row ) { foreach ( array( 'fitted', 'theme', 'skipped', 'images', 'sized' ) as $k ) { $out[ $k ] += (int) ( $row[ $k ] ?? 0 ); } $out['last'] = max( $out['last'], (int) ( $row['time'] ?? 0 ) ); $out['list'][] = array_merge( array( 'url' => (string) $page ), is_array( $row ) ? $row : array() ); } return $out; } }