0 && $b['width'] > 0 && abs( $a['height'] - $a['width'] * $b['height'] / $b['width'] ) <= 1; } /** * Fill only significant gaps. At most four widths per source; reuse native candidates, never upscale or crop. * * @param array $files The picture's files (the original and WordPress's sizes). * @return array The display sizes to make, by key. */ public static function plan( array $files ): array { if ( ! $files ) { return array(); } $family = array_values( $files ); usort( $family, static fn( $a, $b ) => $b['width'] <=> $a['width'] ); $master = null; foreach ( $family as $f ) { if ( 'full' === $f['name'] ) { $master = $f; break; } }$master = $master ?? $family[0]; $family = array_values( array_filter( $family, static fn( $f ) => self::ratio( $master, $f ) ) ); usort( $family, static fn( $a, $b ) => $a['width'] <=> $b['width'] ); $out = array(); foreach ( array( 320, 480, 640, 960, 1280, 1600 ) as $width ) { if ( $width >= $master['width'] ) { continue; }$parent = null; foreach ( $family as $f ) { if ( $f['width'] >= $width ) { $parent = $f; break; } } if ( ! $parent || $parent['width'] / $width <= 1.20 ) { continue; } $height = max( 1, (int) round( $parent['height'] * $width / $parent['width'] ) ); $key = $parent['relative'] . '@display-' . $width; $out[ $key ] = array( 'key' => $key, 'parent' => $parent, 'width' => $width, 'height' => $height, ); if ( count( $out ) >= 4 ) { break; } } return $out; } /** * Sizes measured on the pages (1.8.0): for each width the measured slots need (Fit::needs), an existing or planned size * within 20 % above it; otherwise a new width as large as still covers it. At most six, never the original's width or * more, never a crop; the parent is the smallest existing size of the same shape that is larger. * * @param array $files The picture's files. * @param array $need The widths the measured pages need (Fit::needs). * @return array The display sizes to make, by key. */ public static function fit_plan( array $files, array $need ): array { if ( ! $files || ! $need ) { return array(); } $family = array_values( $files ); usort( $family, static fn( $a, $b ) => $b['width'] <=> $a['width'] ); $master = null; foreach ( $family as $f ) { if ( 'full' === $f['name'] ) { $master = $f; break; } }$master = $master ?? $family[0]; $family = array_values( array_filter( $family, static fn( $f ) => self::ratio( $master, $f ) ) ); usort( $family, static fn( $a, $b ) => $a['width'] <=> $b['width'] ); $widths = array_map( static fn( $f ) => (int) $f['width'], $family ); $out = array(); $need = array_values( array_unique( array_map( 'intval', $need ) ) ); sort( $need ); foreach ( $need as $w ) { if ( $w < 16 || $w >= $master['width'] ) { continue; } $covered = false; foreach ( $widths as $c ) { if ( $c >= $w && $c <= $w * 1.2 ) { $covered = true; break; } } if ( $covered ) { continue; } $width = min( $master['width'] - 1, (int) floor( $w * 1.2 ) ); $parent = null; foreach ( $family as $f ) { if ( $f['width'] > $width ) { $parent = $f; break; } } if ( ! $parent ) { continue; } $height = max( 1, (int) round( $parent['height'] * $width / $parent['width'] ) ); $key = $parent['relative'] . '@display-' . $width; $out[ $key ] = array( 'key' => $key, 'parent' => $parent, 'width' => $width, 'height' => $height, ); $widths[] = $width; if ( count( $out ) >= 6 ) { break; } } return $out; } /** * One extra size per worker operation. Source and publication fences match normal compression. * * @param array $row The source row. * @param array $m Its manifest (or draft), changed in place. * @param array $files The picture's files. * @param array $recipe The compression recipe. * @param string $draft_id The draft being prepared. * @param string $token The worker's lease. * @param array $settings The settings of this run. * @return bool True when a display size was handled in this step, false when none was left. * @throws \RuntimeException Only inside this function, where it is caught; callers get no exception. */ public static function process( array $row, array &$m, array $files, array $recipe, string $draft_id, string $token, array $settings ): bool { $plan = 'fit' === ( $m['sizing'] ?? '' ) ? self::fit_plan( $files, (array) ( $row['details']['fit_need'] ?? array() ) ) : self::plan( $files ); $next = null; foreach ( $plan as $key => $task ) { if ( ! isset( $m['responsive_results'][ $key ] ) ) { $next = $task; break; } } if ( ! $next ) { return false; } $key = $next['key']; $parent = $next['parent']; $tmp = Settings::root( true ) . '/tmp-' . bin2hex( random_bytes( 16 ) ) . '.webp'; $ref = $tmp . '.resize.png'; $created = array(); $guard = null; $a = null; $b = null; $committed = false; try { $receipt = (array) get_option( 'afsd_im_inflight', array() ); $receipt['temp'] = basename( $tmp ); Reviewed_Store::update_option( 'afsd_im_inflight', $receipt, false ); if ( ! self::available() ) { throw new \RuntimeException( 'resize_backend_unavailable' ); } if ( ! in_array( $recipe['formats'], array( 'auto', 'webp' ), true ) ) { throw new \RuntimeException( 'resize_format_held' ); } $input = ImageInput::read( $parent['path'] ); // Unmapped metadata or alpha are never discarded merely to make a smaller width. if ( ! $input['modern_safe'] || $input['metadata'] || $input['alpha'] ) { throw new \RuntimeException( 'resize_metadata_or_alpha_held' ); } // D-253: a JPEG comment and print resolution hold no WebP/AVIF display size (the copy leaves them out, as the // same-size copies do); a display copy in the original format stays held for such a file. $notes_only = ! empty( $input['display_notes'] ); if ( $input['width'] !== $parent['width'] || $input['height'] !== $parent['height'] ) { throw new \RuntimeException( 'source_metadata_dimensions_mismatch' ); } if ( $input['width'] * $input['height'] * 16 + $next['width'] * $next['height'] * 80 + $input['bytes'] * 6 > Settings::memory_budget() ) { throw new \RuntimeException( 'memory_limit' ); } $hash = hash( 'sha256', $input['data'] ); $content = Policy::content( $input, $recipe, $parent['path'] ); $a = imagecreatefromstring( $input['data'] ); $b = imagecreatetruecolor( $next['width'], $next['height'] ); if ( ! $a || ! $b || ! imagecopyresampled( $b, $a, 0, 0, 0, 0, $next['width'], $next['height'], $input['width'], $input['height'] ) || ! imagepng( $b, $ref, 1 ) ) { throw new \RuntimeException( 'resize_failed' ); } // GD adds its own nominal pixel-density chunk to a newly created canvas. // This is our private intermediate, not source metadata. Keep only that known omission. // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Validated local image bytes are processed by the host codec and atomically published only after verification. $refdata = file_get_contents( $ref ); $chunks = ImageInput::png_chunks( $refdata ); $clean = substr( $refdata, 0, 8 ); foreach ( $chunks as $chunk ) { if ( 'pHYs' !== $chunk['type'] ) { $clean .= ImageInput::png_chunk( $chunk['type'], $chunk['data'] ); } } // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- Validated local image bytes are processed by the host codec and atomically published only after verification. if ( Reviewed_Store::file_put_contents( $ref, $clean, LOCK_EX ) !== strlen( $clean ) ) { throw new \RuntimeException( 'resize_failed' ); } clearstatcache( true, $ref ); $dim = wp_getimagesize( $ref ); if ( ! $dim || $dim[0] !== $next['width'] || $dim[1] !== $next['height'] || filesize( $ref ) > Settings::get()['max_bytes'] ) { throw new \RuntimeException( 'resize_failed' ); } // The reference is PNG but photographic content is still photographic. No cascading lossy resaves. $work_recipe = $recipe; $work_recipe['content'] = $content; $r = Compression::encode( $ref, $tmp, $work_recipe, $input['mime'] ); $choices = array_merge( array( $r ), $r['alternatives'] ); usort( $choices, static fn( $x, $y ) => $x['output_bytes'] <=> $y['output_bytes'] ); $usable = array(); foreach ( $choices as $c ) { if ( $notes_only && $c['output_mime'] === $input['mime'] ) { continue; } if ( $c['output_bytes'] + 1024 > $input['bytes'] || $c['output_bytes'] > $input['bytes'] * ( 1 - $settings['min_saving'] / 100 ) ) { continue; } // Do not create a new format/width which no complete family can advertise. if ( $c['output_mime'] !== $input['mime'] ) { $covered = true; foreach ( $files as $f ) { if ( ! self::ratio( $parent, $f ) ) { continue; }$found = false; foreach ( Variants::choices( $m['variants'][ $f['relative'] ] ?? array() ) as $base ) { if ( Variants::mime( $base ) === $c['output_mime'] && Delivery::usable( $base, $f ) ) { $found = true; break; } } if ( ! $found ) { $covered = false; break; } } if ( ! $covered ) { continue; } } $usable[] = $c; } if ( ! $usable ) { throw new \RuntimeException( 'resize_no_complete_family' ); } // Keep the smallest plus an original-format fallback if different; not every format at every width. $chosen = array( $usable[0] ); foreach ( $usable as $c ) { if ( $c['output_mime'] === $input['mime'] && $c['output_mime'] !== $chosen[0]['output_mime'] ) { $chosen[] = $c; break; } } $journal = array(); $temporary = array(); foreach ( $chosen as $r ) { $r['resized'] = true; $r['resampler'] = self::VERSION; $r['parent_relative'] = $parent['relative']; $r['source_width'] = $input['width']; $r['source_height'] = $input['height']; $r['reference_bytes'] = $r['bytes']; $r['reference_hash'] = $r['source_hash']; $r['bytes'] = $input['bytes']; $r['source_hash'] = $hash; $r['source_mtime'] = (int) filemtime( $parent['path'] ); $r['saving_bytes'] = 0; $r['verification'] = 'Display resize, no crop; compression validated against the same-size reference.'; $r['name'] = 'Display ' . $next['width'] . 'px'; $r['created'] = time(); $path = $r['_temporary']; unset( $r['_temporary'], $r['alternatives'] ); $identity = hash( 'sha256', self::VERSION . '|' . $hash . '|' . $r['width'] . 'x' . $r['height'] . '|' . $r['output_hash'] ); $r['file'] = Sources::prefix( $row ) . '-' . $identity . '.' . Variants::extension( $r['output_mime'] ); $journal[] = $r; $temporary[ $r['file'] ] = $path; } $guard = Engine::publication_lock(); $fresh = Repository::row( (int) $row['id'] ); clearstatcache( true, $parent['path'] ); if ( ! $fresh || $fresh['excluded'] || ! Repository::owns( $token ) || (int) $fresh['revision'] !== (int) $row['revision'] || Settings::fresh()['generation'] !== $settings['generation'] || ! Sources::eligible( $fresh ) || ! is_file( $parent['path'] ) || ! hash_equals( $hash, hash_file( 'sha256', $parent['path'] ) ) || ( $draft_id && ( $fresh['manifest']['draft']['id'] ?? '' ) !== $draft_id ) ) { throw new \RuntimeException( 'stale_job' ); } $m['pending'] = $journal; if ( ! Engine::work_update( $row, array( 'manifest' => $m ), $draft_id ) ) { throw new \RuntimeException( 'stale_job' ); } foreach ( $journal as $r ) { $dest = Settings::root() . '/' . $r['file']; if ( is_link( $dest ) ) { throw new \RuntimeException( 'unsafe_storage' ); } if ( is_file( $dest ) ) { if ( ! hash_equals( $r['output_hash'], hash_file( 'sha256', $dest ) ) ) { throw new \RuntimeException( 'output_name_collision' ); } } // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_chmod,WordPress.WP.AlternativeFunctions.rename_rename -- Validated local image bytes are processed by the host codec and atomically published only after verification. else { if ( ! Reviewed_Store::rename( $temporary[ $r['file'] ], $dest ) ) { throw new \RuntimeException( 'atomic_publish_failed' ); }$created[] = $dest; // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_chmod -- Validated local image bytes are processed by the host codec and atomically published only after verification. }chmod( $dest, 0644 ); } $r = array_shift( $journal ); $r['alternatives'] = $journal; $m['pending'] = array(); $m['responsive'][ $key ] = $r; $m['responsive_results'][ $key ] = array( 'state' => 'ready', 'reason' => 'Added ' . $r['width'] . ' × ' . $r['height'] . ' display size. Originals unchanged.', ); $committed = Engine::work_update( $row, array( 'manifest' => $m, 'available_at' => time(), 'attempts' => 0, ), $draft_id ); } catch ( \Throwable $e ) { if ( 'stale_job' !== $e->getMessage() ) { $m['responsive_results'][ $key ] = array( 'state' => 'unsupported', 'reason' => sanitize_key( $e->getMessage() ), ); Engine::work_update( $row, array( 'manifest' => $m, 'available_at' => time(), ), $draft_id ); } } finally { if ( $a ) { imagedestroy( $a ); }if ( $b ) { imagedestroy( $b ); }Compression::cleanup_temporary( $tmp ); if ( ! $committed ) { foreach ( $created as $path ) { if ( is_file( $path ) && ! is_link( $path ) ) { Reviewed_Store::wp_delete_file( $path ); } } }Engine::publication_unlock( $guard ); } return true; } /** * The picture's files with the display sizes of its draft added, for the comparison preview. * * @param array $row The source row. * @param array $files The picture's files. * @return array The files, display sizes included. */ public static function preview_files( array $row, array $files ): array { foreach ( $row['manifest']['draft']['responsive'] ?? array() as $key => $v ) { if ( isset( $files[ $v['parent_relative'] ?? '' ] ) ) { $f = $files[ $v['parent_relative'] ]; $f['relative'] = $key; $f['name'] = 'Display ' . $v['width'] . 'px'; $f['display_width'] = $v['width']; $f['display_height'] = $v['height']; $files[ $key ] = $f; } } return $files; } /** * Additional candidates can only refine the interval for their exact native parent. No new crop family. * * @param array $sets The source sets delivery built. * @param array $entries The native files of the img. * @param array $row The source row. * @param string $sizes The img's sizes attribute. * @return array The source sets with the display sizes added. */ public static function extend( array $sets, array $entries, array $row, string $sizes ): array { if ( ! $sizes || empty( $row['manifest']['responsive'] ) ) { return $sets; } $native = array(); foreach ( $entries as $e ) { $width = $e['width'] ?: $e['file']['width']; $native[ $width ] = $e['file']; }ksort( $native ); foreach ( $sets as &$set ) { $parts = Urls::srcset( $set['srcset'] ); if ( ! $parts && count( $native ) === 1 ) { $w = (int) array_key_first( $native ); $parts = array( array( 'url' => $set['srcset'], 'width' => $w, ), ); } $known = array_column( $parts, 'width' ); $extra = array(); foreach ( $row['manifest']['responsive'] as $family ) { foreach ( Variants::choices( $family ) as $v ) { if ( Variants::mime( $v ) !== $set['mime'] || in_array( (int) $v['width'], $known, true ) ) { continue; } $parent = null; foreach ( $native as $width => $f ) { if ( $width >= $v['width'] ) { $parent = $f; break; } } if ( ! $parent || $parent['relative'] !== ( $v['parent_relative'] ?? '' ) || ! Delivery::usable( $v, $parent ) ) { continue; } $extra[] = array( 'url' => Delivery::url( $v ), 'width' => (int) $v['width'], ); } } if ( $extra ) { $set['managed'] = (int) ( $set['managed'] ?? 0 ) + count( $extra ); $parts = array_merge( $parts, $extra ); usort( $parts, static fn( $a, $b ) => $a['width'] <=> $b['width'] ); $set['srcset'] = implode( ', ', array_map( static fn( $v ) => $v['url'] . ' ' . $v['width'] . 'w', $parts ) ); } }unset( $set ); return array_values( array_filter( $sets, static fn( $set ) => ! isset( $set['managed'] ) || $set['managed'] > 0 ) ); } }