-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add avif support #28
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from 3 commits
20fc547
66e2886
bb79d6f
e1546b0
d96f055
f6aed4e
2026c9c
4b44ed2
caeba2b
d075d5f
483ac40
1273c4d
e5e0845
f3458cb
09e2a71
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,10 +1,13 @@ | ||
| import { Converter } from './converter-abstract' | ||
| import { encode as encodeAvif } from '@jsquash/avif' | ||
| import { isFormatSupported } from './util' | ||
|
|
||
| // Supported output formats | ||
| const supportedFormats = [ | ||
| { value: 'webp', mimeType: 'image/webp' }, | ||
| { value: 'jpg', mimeType: 'image/jpeg' }, | ||
| { value: 'png', mimeType: 'image/png' }, | ||
| { value: 'avif', mimeType: 'image/avif' }, | ||
| ] | ||
|
|
||
| /** | ||
|
|
@@ -19,6 +22,7 @@ class ImageConverter extends Converter { | |
| 'image/png', | ||
| 'image/webp', | ||
| 'image/jpg', | ||
| 'image/avif', | ||
| ] | ||
| } | ||
|
|
||
|
|
@@ -171,24 +175,36 @@ class ImageConverter extends Converter { | |
|
|
||
| const format = supportedFormats.find( f => f.value === outputFormat ) | ||
| // Only use quality for lossy formats | ||
| const q = ( outputFormat === 'webp' || outputFormat === 'jpg' ) ? quality : undefined | ||
|
|
||
| canvas.toBlob( function( blob ) { | ||
| // Clean up resources | ||
| URL.revokeObjectURL( objectUrl ) | ||
| objectUrl = null | ||
|
|
||
| // Clear canvas to free memory | ||
| ctx.clearRect( 0, 0, canvas.width, canvas.height ) | ||
| canvas.width = 0 | ||
| canvas.height = 0 | ||
|
|
||
| if ( blob ) { | ||
| resolve( blob ) | ||
| } else { | ||
| reject( new Error( 'Failed to convert image' ) ) | ||
| const q = ( outputFormat === 'webp' || outputFormat === 'jpg' || outputFormat === 'avif' ) ? quality : undefined | ||
|
|
||
| if ( outputFormat === 'avif' ) { | ||
| try { | ||
| const imageData = ctx.getImageData( 0, 0, width, height ) | ||
| const avifQuality = Math.max( 1, Math.min( 100, Math.round( ( ( q ?? 0.5 ) * 100 ) ) ) ) | ||
| const avifBuffer = await encodeAvif( imageData, { quality: avifQuality } ) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result:
Practical mapping (if you have a 0–100 “quality” and want a // quality: 0..100 where 100 ~= best
const cqLevel = Math.round((100 - quality) * 63 / 100);(This mapping is approximate; different encoders/settings can shift results.) Sources: [1] Fix AVIF quality parameter: replace
Map quality (0–1) to 🤖 Prompt for AI Agents
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. https://github.com/jamsinclair/jSquash/blob/main/packages/avif/codec/enc/avif_enc.d.ts#L7 @jsquash/avif's encode has a quality parameter There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| const avifBlob = new Blob( [ avifBuffer ], { type: 'image/avif' } ) | ||
| resolve( avifBlob ) | ||
| } catch ( e ) { | ||
| reject( new Error( `Failed to encode AVIF: ${ e instanceof Error ? e.message : 'Unknown error' }` ) ) | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Resource leak: AVIF path skips The non-AVIF branch (lines 192-199) revokes the object URL and zeros the canvas to free memory, but the AVIF branch resolves/rejects without doing either. This leaks the blob URL and keeps the canvas buffer alive. Proposed fix if ( outputFormat === 'avif' ) {
try {
const imageData = ctx.getImageData( 0, 0, width, height )
const avifQuality = Math.max( 1, Math.min( 100, Math.round( ( ( q ?? 0.5 ) * 100 ) ) ) )
const avifBuffer = await encodeAvif( imageData, { quality: avifQuality } )
const avifBlob = new Blob( [ avifBuffer ], { type: 'image/avif' } )
+ // Clean up resources
+ URL.revokeObjectURL( objectUrl )
+ objectUrl = null
+ ctx.clearRect( 0, 0, canvas.width, canvas.height )
+ canvas.width = 0
+ canvas.height = 0
resolve( avifBlob )
} catch ( e ) {
+ URL.revokeObjectURL( objectUrl )
+ objectUrl = null
reject( new Error( `Failed to encode AVIF: ${ e instanceof Error ? e.message : 'Unknown error' }` ) )
}🤖 Prompt for AI Agents |
||
| }, format.mimeType, q ) | ||
| } else { | ||
| canvas.toBlob( function( blob ) { | ||
| // Clean up resources | ||
| URL.revokeObjectURL( objectUrl ) | ||
| objectUrl = null | ||
|
|
||
| // Clear canvas to free memory | ||
| ctx.clearRect( 0, 0, canvas.width, canvas.height ) | ||
| canvas.width = 0 | ||
| canvas.height = 0 | ||
|
|
||
| if ( blob ) { | ||
| resolve( blob ) | ||
| } else { | ||
| reject( new Error( 'Failed to convert image' ) ) | ||
| } | ||
| }, format.mimeType, q ) | ||
| } | ||
| } | ||
|
|
||
| img.onerror = () => { | ||
|
|
@@ -225,8 +241,7 @@ class ImageConverter extends Converter { | |
| } | ||
|
|
||
| // Check if the browser supports the desired output format | ||
| const testCanvas = document.createElement( 'canvas' ) | ||
| if ( formatInfo && ! testCanvas.toDataURL( formatInfo.mimeType ).startsWith( `data:${ formatInfo.mimeType }` ) ) { | ||
| if ( ! await isFormatSupported( format ) ) { | ||
| // If not supported, skip conversion and return the original file | ||
| // eslint-disable-next-line no-console | ||
| console.error( '[Cimo] ' + format + ' is not supported by the browser, please use another modern browser' ) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| import { ImageConverter } from './image-converter' | ||
| import { NullConverter } from './null-converter' | ||
| import { applyFilters } from '@wordpress/hooks' | ||
| import { isFormatSupported } from './util' | ||
|
|
||
| /** | ||
| * Get the file converter for the given file. | ||
|
|
@@ -38,13 +39,14 @@ export const getFileConverter = _file => { | |
| } | ||
|
|
||
| if ( file.type.startsWith( 'image/' ) ) { | ||
| // If the browser doesn't support webp, then we can't convert it. | ||
| if ( ! isFormatSupported( 'webp' ) ) { | ||
| const imageOutputFormat = window.cimoSettings?.imageOutputFormat || 'webp' | ||
| // If the browser doesn't support set output format, then we can't convert it. | ||
| if ( ! isFormatSupported( imageOutputFormat ) ) { | ||
| return new NullConverter( file ) | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Critical:
Since
🤖 Prompt for AI Agents |
||
| if ( ImageConverter.supportsMimeType( file.type ) ) { | ||
| return new ImageConverter( file, { | ||
| format: 'webp', | ||
| format: imageOutputFormat, | ||
| quality: window.cimoSettings?.webpQuality || 0.8, | ||
| maxDimension: window.cimoSettings?.maxImageDimension || 0, | ||
| } ) | ||
|
|
@@ -67,41 +69,3 @@ export function requiresFileConversion( converters ) { | |
| return ! converters.every( converter => converter.constructor.name === 'NullConverter' ) | ||
| } | ||
|
|
||
| /** | ||
| * Check if a specific image format is supported by the browser | ||
| * | ||
| * @param {string} format - Format name ('webp', 'jpg', 'png', 'avif') or MIME type ('image/webp') | ||
| * @return {boolean} - True if format is supported, false otherwise | ||
| */ | ||
| function isFormatSupported( format ) { | ||
| if ( ! format || typeof format !== 'string' ) { | ||
| return false | ||
| } | ||
|
|
||
| // Map format names to MIME types | ||
| const formatMap = { | ||
| webp: 'image/webp', | ||
| avif: 'image/avif', | ||
| } | ||
|
|
||
| // Get MIME type (either from map or use as-is if already a MIME type) | ||
| const mimeType = formatMap[ format.toLowerCase() ] || ( format.startsWith( 'image/' ) ? format : null ) | ||
|
|
||
| if ( ! mimeType ) { | ||
| return false | ||
| } | ||
|
|
||
| // Create a test canvas and check if toDataURL supports the format | ||
| const canvas = document.createElement( 'canvas' ) | ||
| canvas.width = 1 | ||
| canvas.height = 1 | ||
|
|
||
| try { | ||
| const dataUrl = canvas.toDataURL( mimeType ) | ||
| // If the browser doesn't support the format, it falls back to image/png | ||
| // Check if the data URL starts with the requested mime type | ||
| return dataUrl.startsWith( `data:${ mimeType }` ) | ||
| } catch ( e ) { | ||
| return false | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,76 @@ | ||||||||||||||||||||||||||||||||||||||
| // Cache for format support results to avoid redundant checks | ||||||||||||||||||||||||||||||||||||||
| const formatSupportCache = {} | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||
| * Check if a specific image format is supported by the browser | ||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||
| * @param {string} format - Format name ('webp', 'jpg', 'png', 'avif') or MIME type ('image/webp') | ||||||||||||||||||||||||||||||||||||||
| * @return {boolean} - True if format is supported, false otherwise | ||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||
| export async function isFormatSupported( format ) { | ||||||||||||||||||||||||||||||||||||||
| if ( ! format || typeof format !== 'string' ) { | ||||||||||||||||||||||||||||||||||||||
| return false | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| const key = format.toLowerCase() | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| // Return cached result if already checked | ||||||||||||||||||||||||||||||||||||||
| if ( key in formatSupportCache ) { | ||||||||||||||||||||||||||||||||||||||
| return formatSupportCache[ key ] | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| // Map format names to MIME types | ||||||||||||||||||||||||||||||||||||||
| const formatMap = { | ||||||||||||||||||||||||||||||||||||||
| webp: 'image/webp', | ||||||||||||||||||||||||||||||||||||||
| avif: 'image/avif', | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| // Get MIME type (either from map or use as-is if already a MIME type) | ||||||||||||||||||||||||||||||||||||||
| const mimeType = | ||||||||||||||||||||||||||||||||||||||
| formatMap[ key ] || ( key.startsWith( 'image/' ) ? key : null ) | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| if ( ! mimeType ) { | ||||||||||||||||||||||||||||||||||||||
| formatSupportCache[ key ] = false | ||||||||||||||||||||||||||||||||||||||
| return false | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| let supported = false | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| // Check for AVIF decoding support first since it may not be supported even | ||||||||||||||||||||||||||||||||||||||
| // if the library can encode it. If the browser can't decode it, then | ||||||||||||||||||||||||||||||||||||||
| // it's not useful to convert to AVIF. | ||||||||||||||||||||||||||||||||||||||
| if ( key === 'avif' ) { | ||||||||||||||||||||||||||||||||||||||
| supported = await supportsAvifDecode() | ||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||
| // Create a test canvas and check if toDataURL supports the format | ||||||||||||||||||||||||||||||||||||||
| const canvas = document.createElement( 'canvas' ) | ||||||||||||||||||||||||||||||||||||||
| canvas.width = 1 | ||||||||||||||||||||||||||||||||||||||
| canvas.height = 1 | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||
| const dataUrl = canvas.toDataURL( mimeType ) | ||||||||||||||||||||||||||||||||||||||
| // If the browser doesn't support the format, it falls back to image/png | ||||||||||||||||||||||||||||||||||||||
| // Check if the data URL starts with the requested mime type | ||||||||||||||||||||||||||||||||||||||
| supported = dataUrl.startsWith( `data:${ mimeType }` ) | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| return supported | ||||||||||||||||||||||||||||||||||||||
| } catch ( e ) { | ||||||||||||||||||||||||||||||||||||||
| supported = false | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+50
to
+57
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: successful canvas-based check bypasses the cache. Line 56 returns early without writing to Remove the early return so both the success and error paths fall through to the shared cache-write on line 62. Proposed fix try {
const dataUrl = canvas.toDataURL( mimeType )
// If the browser doesn't support the format, it falls back to image/png
// Check if the data URL starts with the requested mime type
supported = dataUrl.startsWith( `data:${ mimeType }` )
-
- return supported
} catch ( e ) {
supported = false
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| formatSupportCache[ key ] = supported | ||||||||||||||||||||||||||||||||||||||
| return supported | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| function supportsAvifDecode() { | ||||||||||||||||||||||||||||||||||||||
| return new Promise( resolve => { | ||||||||||||||||||||||||||||||||||||||
| const img = new Image() | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| img.onload = () => resolve( true ) | ||||||||||||||||||||||||||||||||||||||
| img.onerror = () => resolve( false ) | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| img.src = | ||||||||||||||||||||||||||||||||||||||
| 'data:image/avif;base64,AAAAIGZ0eXBhdmlmAAAAAGF2aWZtaWYxbWlhZk1BMUIAAADybWV0YQAAAAAAAAAoaGRscgAAAAAAAAAAcGljdAAAAAAAAAAAAAAAAGxpYmF2aWYAAAAADnBpdG0AAAAAAAEAAAAeaWxvYwAAAABEAAABAAEAAAABAAABGgAAAB0AAAAoaWluZgAAAAAAAQAAABppbmZlAgAAAAABAABhdjAxQ29sb3IAAAAAamlwcnAAAABLaXBjbwAAABRpc3BlAAAAAAAAAAIAAAACAAAAEHBpeGkAAAAAAwgICAAAAAxhdjFDgQ0MAAAAABNjb2xybmNseAACAAIAAYAAAAAXaXBtYQAAAAAAAAABAAEEAQKDBAAAACVtZGF0EgAKCBgANogQEAwgMg8f8D///8WfhwB8+ErK42A=' | ||||||||||||||||||||||||||||||||||||||
| } ) | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Restrict
image_output_formatto an allowlist of valid values.Currently the schema has no
enumconstraint and the sanitizer only callssanitize_text_field, meaning any arbitrary string (including malicious input) can be persisted. Since only'webp'and'avif'are valid, add validation at both layers:Proposed fix
In the REST schema (around line 59):
'image_output_format' => [ 'type' => 'string', + 'enum' => [ 'webp', 'avif' ], ],In
sanitize_options(around line 225):if ( isset( $options['image_output_format'] ) ) { - $sanitized['image_output_format'] = sanitize_text_field( $options['image_output_format'] ); + $format = sanitize_text_field( $options['image_output_format'] ); + $sanitized['image_output_format'] = in_array( $format, [ 'webp', 'avif' ], true ) ? $format : 'webp'; }Also applies to: 224-227
🤖 Prompt for AI Agents