From 12fbe819de24a925046f9993e973446121a8b0b1 Mon Sep 17 00:00:00 2001 From: skjnldsv Date: Fri, 18 Sep 2026 10:58:05 +0200 Subject: [PATCH] feat(images): open AVIF Every engine the viewer runs in decodes AVIF natively, so it belongs with the formats shown from the file itself rather than from a preview. Probed rather than assumed: a 64x64 AVIF decodes through both an img element and createImageBitmap in Chromium and in Firefox. No Nextcloud preview provider covers it, so listing it as preview-supported would have kept it unopenable everywhere. Closes nextcloud/viewer#407 Co-authored-by: JanisPlayer Assisted-by: ClaudeCode:claude-opus-5 Signed-off-by: skjnldsv --- __tests__/models.spec.ts | 1 + e2e/formats.spec.ts | 28 ++++++++++++++++++ e2e/navigation.spec.ts | 2 +- lib/models/images.ts | 4 +++ playground/App.vue | 1 + .../dav/files/playground/photo.avif | Bin 0 -> 450 bytes 6 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 e2e/formats.spec.ts create mode 100644 playground/public/remote.php/dav/files/playground/photo.avif 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 0000000000000000000000000000000000000000..ccccf3d31d7004afece5eede7cf3ebc531706941 GIT binary patch literal 450 zcmZQzV30{GsVqn=%S>Yc0uY^>nP!-qnF!*)&P^>z1c?KIVn#|%5roOWP>`8i0%OBy zzJkn>Trh_bNXljAzrX-dKF>rB3C_exD zHiJXJ#KCA0qf3XHfsZ4LfXS*A3=D?^G(V;+VGd(gns~6}fh-T7w#aj}eFYL$f4|IS#2X-3hGdza6wdg1VwQ>u6pkFe`x)phH%%qx2@ c+HdOI^5>Z3cA-mex$0!n%j3mo99!!E09=25n*aa+ literal 0 HcmV?d00001