Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions avif-local-support.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ function avif_local_support_activate(): void
add_option('avif_local_support_preserve_color_profile', true);
add_option('avif_local_support_wordpress_logic', true);
add_option('avif_local_support_cache_duration', 3600);
add_option('avif_local_support_chroma_subsampling', '4:2:0');
add_option('avif_local_support_bit_depth', 8);
}

function avif_local_support_deactivate(): void
Expand Down Expand Up @@ -99,6 +101,8 @@ function avif_local_support_uninstall(): void
'avif_local_support_preserve_color_profile',
'avif_local_support_wordpress_logic',
'avif_local_support_cache_duration',
'avif_local_support_chroma_subsampling',
'avif_local_support_bit_depth',
];

foreach ($options as $option) {
Expand Down
71 changes: 71 additions & 0 deletions includes/class-avif-suite.php
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,63 @@ function (): void {
'avif_local_support_conversion',
[ 'label_for' => 'avif_local_support_wordpress_logic' ]
);

// Advanced options section
register_setting('avif_local_support_settings', 'avif_local_support_chroma_subsampling', [
'type' => 'string',
'default' => '4:2:0',
'sanitize_callback' => [$this, 'sanitize_chroma_subsampling'],
'show_in_rest' => true,
]);
register_setting('avif_local_support_settings', 'avif_local_support_bit_depth', [
'type' => 'integer',
'default' => 8,
'sanitize_callback' => [$this, 'sanitize_bit_depth'],
'show_in_rest' => true,
]);

add_settings_section(
'avif_local_support_advanced',
'',
function (): void {},
'avif-local-support'
);

add_settings_field(
'avif_local_support_chroma_subsampling',
__('Chroma subsampling', 'avif-local-support'),
function (): void {
$value = (string) get_option('avif_local_support_chroma_subsampling', '4:2:0');
$options = ['4:2:0', '4:2:2', '4:4:4'];
echo '<select id="avif_local_support_chroma_subsampling" name="avif_local_support_chroma_subsampling">';
foreach ($options as $opt) {
echo '<option value="' . esc_attr($opt) . '" ' . selected($opt, $value, false) . '>' . esc_html($opt) . '</option>';
}
echo '</select>';
echo '<p class="description">' . esc_html__('Lower subsampling reduces file size; 4:4:4 preserves the most color detail (best for graphics/text).', 'avif-local-support') . '</p>';
},
'avif-local-support',
'avif_local_support_advanced',
[ 'label_for' => 'avif_local_support_chroma_subsampling' ]
);

add_settings_field(
'avif_local_support_bit_depth',
__('Bit depth', 'avif-local-support'),
function (): void {
$value = (int) get_option('avif_local_support_bit_depth', 8);
$options = [8, 10, 12];
echo '<select id="avif_local_support_bit_depth" name="avif_local_support_bit_depth">';
foreach ($options as $opt) {
echo '<option value="' . esc_attr((string) $opt) . '" ' . selected($opt, $value, false) . '>' . esc_html((string) $opt) . '</option>';
}
echo '</select>';
echo '<p class="description">' . esc_html__('Higher bit depths can reduce banding and improve gradients (requires Imagick with AVIF support).', 'avif-local-support') . '</p>';
},
'avif-local-support',
'avif_local_support_advanced',
[ 'label_for' => 'avif_local_support_bit_depth' ]
);
}

public function sanitize_schedule_time(string $value): string
Expand All @@ -285,6 +342,20 @@ public function sanitize_speed($value): int
return $n;
}

public function sanitize_chroma_subsampling($value): string
{
$val = is_string($value) ? $value : '';
$allowed = ['4:2:0', '4:2:2', '4:4:4'];
return in_array($val, $allowed, true) ? $val : '4:2:0';
}

public function sanitize_bit_depth($value): int
{
$n = (int) $value;
$allowed = [8, 10, 12];
return in_array($n, $allowed, true) ? $n : 8;
}

public function render_admin_page(): void
{
if (!current_user_can('manage_options')) {
Expand Down
60 changes: 59 additions & 1 deletion includes/class-converter.php
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,10 @@ private function convertToAvif(string $sourcePath, string $avifPath, ?array $tar
$speedSetting = max(0, min(10, (int) get_option('avif_local_support_speed', 1)));
$preserveMeta = (bool) get_option('avif_local_support_preserve_metadata', true);
$preserveICC = (bool) get_option('avif_local_support_preserve_color_profile', true);
$chromaSubsampling = (string) get_option('avif_local_support_chroma_subsampling', '4:2:0');
if (!in_array($chromaSubsampling, ['4:2:0','4:2:2','4:4:4'], true)) { $chromaSubsampling = '4:2:0'; }
$bitDepthSetting = (int) get_option('avif_local_support_bit_depth', 8);
if (!in_array($bitDepthSetting, [8,10,12], true)) { $bitDepthSetting = 8; }

// Ensure directory exists
$dir = \dirname($avifPath);
Expand All @@ -156,6 +160,10 @@ private function convertToAvif(string $sourcePath, string $avifPath, ?array $tar
}
}

// Detect whether source has an embedded ICC profile
$hasIcc = false;
try { $maybeIcc = $im->getImageProfile('icc'); $hasIcc = !empty($maybeIcc); } catch (\Throwable $e) {}

if ($targetDimensions && isset($targetDimensions['width'], $targetDimensions['height'])) {
$srcW = $im->getImageWidth();
$srcH = $im->getImageHeight();
Expand All @@ -175,19 +183,47 @@ private function convertToAvif(string $sourcePath, string $avifPath, ?array $tar
$cropY = (int) (($srcH - $cropH) / 2);
}
$im->cropImage($cropW, $cropH, $cropX, $cropY);

// Tweak Lanczos filter and use linear-light resize for untagged images
try { $im->setOption('filter:blur', '0.985'); } catch (\Throwable $e) {}
try { $im->setOption('filter:window', 'Jinc'); } catch (\Throwable $e) {}
try { $im->setOption('filter:lobes', '3'); } catch (\Throwable $e) {}
if (!$hasIcc) {
try { $im->transformImageColorspace(\Imagick::COLORSPACE_RGB); } catch (\Throwable $e) {}
}
$im->resizeImage($tW, $tH, \Imagick::FILTER_LANCZOS, 1.0);
if (!$hasIcc) {
try { $im->transformImageColorspace(\Imagick::COLORSPACE_SRGB); } catch (\Throwable $e) {}
}
// Mild post-resize sharpening to restore micro-contrast
try { $im->unsharpMaskImage(0.0, 0.8, 0.9, 0.02); } catch (\Throwable $e) {}
}

$im->setImageFormat('AVIF');
$im->setImageCompressionQuality($quality);
$im->setOption('avif:speed', (string) min(8, $speedSetting));
// Advanced: chroma subsampling and bit depth
$im->setOption('avif:chroma-subsampling', $chromaSubsampling);
$im->setOption('avif:depth', (string) $bitDepthSetting);
try { $im->setImageDepth($bitDepthSetting); } catch (\Throwable $e) {}

if ($preserveMeta || $preserveICC) {
try {
$src = new \Imagick($sourcePath);
if ($preserveICC) {
$icc = $src->getImageProfile('icc');
if (!empty($icc)) { $im->setImageProfile('icc', $icc); }
if (!empty($icc)) {
$im->setImageProfile('icc', $icc);
} else {
// Assign sRGB only (do not transform) when no ICC exists
$srgbPath = $this->findSrgbProfile();
if ($srgbPath && is_readable($srgbPath)) {
$srgbBytes = @file_get_contents($srgbPath);
if ($srgbBytes !== false && $srgbBytes !== '') {
$im->setImageProfile('icc', $srgbBytes);
}
}
}
}
if ($preserveMeta) {
foreach (['exif', 'xmp', 'iptc'] as $profile) {
Expand Down Expand Up @@ -482,4 +518,26 @@ public function convertAttachmentNow(int $attachmentId): array

return $results;
}

/**
* Try to locate a system sRGB ICC profile to tag unprofiled images without converting pixels.
*/
private function findSrgbProfile(): ?string
{
$candidates = [
'/usr/share/color/icc/sRGB.icc',
'/usr/share/color/icc/colord/sRGB.icc',
'/usr/share/color/icc/ghostscript/sRGB.icc',
'/usr/local/share/color/icc/sRGB.icc',
'/usr/share/icc/sRGB.icc',
'C:\\Windows\\System32\\spool\\drivers\\color\\sRGB Color Space Profile.icm',
'/System/Library/ColorSync/Profiles/sRGB Profile.icc',
];
foreach ($candidates as $path) {
if (@is_file($path) && @is_readable($path)) {
return $path;
}
}
return null;
}
}