diff --git a/avif-local-support.php b/avif-local-support.php index d8887fe..e172b29 100644 --- a/avif-local-support.php +++ b/avif-local-support.php @@ -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 @@ -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) { diff --git a/includes/class-avif-suite.php b/includes/class-avif-suite.php index 877fa8e..e67baf8 100644 --- a/includes/class-avif-suite.php +++ b/includes/class-avif-suite.php @@ -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 ''; + echo '
' . esc_html__('Lower subsampling reduces file size; 4:4:4 preserves the most color detail (best for graphics/text).', 'avif-local-support') . '
'; + }, + '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 ''; + echo '' . esc_html__('Higher bit depths can reduce banding and improve gradients (requires Imagick with AVIF support).', 'avif-local-support') . '
'; + }, + 'avif-local-support', + 'avif_local_support_advanced', + [ 'label_for' => 'avif_local_support_bit_depth' ] + ); } public function sanitize_schedule_time(string $value): string @@ -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')) { diff --git a/includes/class-converter.php b/includes/class-converter.php index a641ef1..3f20920 100644 --- a/includes/class-converter.php +++ b/includes/class-converter.php @@ -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); @@ -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(); @@ -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) { @@ -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; + } }