diff --git a/__tests__/models.spec.ts b/__tests__/models.spec.ts index 129e19f..51eaf0f 100644 --- a/__tests__/models.spec.ts +++ b/__tests__/models.spec.ts @@ -159,6 +159,7 @@ describe('audios model', () => { describe('images model', () => { it.each([ 'image/apng', + 'image/avif', 'image/bmp', 'image/gif', 'image/jpeg', diff --git a/e2e/formats.spec.ts b/e2e/formats.spec.ts new file mode 100644 index 0000000..fb0a0ca --- /dev/null +++ b/e2e/formats.spec.ts @@ -0,0 +1,28 @@ +/*! + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +import { expect, test } from '@playwright/test' +import { ViewerPage } from './support/viewer.ts' + +test.describe('Formats the browser decodes itself', () => { + test('opens an AVIF and paints it', async ({ page }) => { + const viewer = new ViewerPage(page) + await viewer.open('photo.avif') + await viewer.waitForOpen() + + // Listed as browser-supported, so no preview stands behind it: the + // engine either decodes the file or the viewer shows nothing. Asking + // the element for its intrinsic size is asking whether it decoded. + const image = viewer.container.locator('img').first() + await expect(image).toBeVisible() + await expect(async () => { + const decoded = await image.evaluate((element: HTMLImageElement) => ({ + complete: element.complete, + width: element.naturalWidth, + height: element.naturalHeight, + })) + expect(decoded).toEqual({ complete: true, width: 320, height: 240 }) + }).toPass({ timeout: 5000 }) + }) +}) diff --git a/e2e/navigation.spec.ts b/e2e/navigation.spec.ts index e3f04de..928d728 100644 --- a/e2e/navigation.spec.ts +++ b/e2e/navigation.spec.ts @@ -7,7 +7,7 @@ import { ViewerPage } from './support/viewer.ts' // The order the playground lists them in, which is the order the viewer is // handed and the order it has to step through -const IMAGES = ['photo.jpg', 'gradient.jpg', 'portrait.jpg', 'animation.gif', 'protected.jpg'] +const IMAGES = ['photo.jpg', 'gradient.jpg', 'portrait.jpg', 'photo.avif', 'animation.gif', 'protected.jpg'] test.describe('Viewer navigation', () => { test('steps through the list and loops around at both ends', async ({ page }) => { diff --git a/lib/models/images.ts b/lib/models/images.ts index 559ea7e..1009f0e 100644 --- a/lib/models/images.ts +++ b/lib/models/images.ts @@ -39,6 +39,10 @@ const previewSupportedMimes = [ */ const browserSupportedMimes = [ 'image/apng', + // Decoded natively by every engine the viewer runs in, so it needs no + // preview: there is no provider for it either, and waiting for one + // would keep it unopenable on servers that will never have it + 'image/avif', 'image/bmp', 'image/gif', 'image/jpeg', diff --git a/playground/App.vue b/playground/App.vue index 884d5e2..4a5c9b8 100644 --- a/playground/App.vue +++ b/playground/App.vue @@ -40,6 +40,7 @@ const fixtures: Fixture[] = [ ...(withPreviews ? [{ name: 'previewed.jpg', mime: 'image/jpeg', hasPreview: true }] : []), { name: 'gradient.jpg', mime: 'image/jpeg', editable: true }, { name: 'portrait.jpg', mime: 'image/jpeg', editable: true }, + { name: 'photo.avif', mime: 'image/avif', editable: true }, { name: 'animation.gif', mime: 'image/gif' }, { name: 'protected.jpg', mime: 'image/jpeg', noDownload: true }, { name: 'video.mp4', mime: 'video/mp4' }, diff --git a/playground/public/remote.php/dav/files/playground/photo.avif b/playground/public/remote.php/dav/files/playground/photo.avif new file mode 100644 index 0000000..ccccf3d Binary files /dev/null and b/playground/public/remote.php/dav/files/playground/photo.avif differ