fix(history): let the cover ask for JPEG XL when this browser decodes it
Firefox can decode JPEG XL without listing image/jxl in Accept. When the workspace's probe decodes JPEG XL, it adds jxl=1 to the History cover URL. The server serves a .jxl preview when that flag is set or when Accept lists image/jxl. In every other case it falls back as before. The flag accepts only 0 or 1, and the response stays no-store.
This commit is contained in:
+4
-2
@@ -2360,8 +2360,10 @@ Thumbnail format and settings (#317, `py/api/thumbnail_settings.py`,
|
||||
preview `<img>` that fails to load falls back to the original (the legacy gallery
|
||||
already does). A video whose preview it cannot show keeps its own element (Images) or
|
||||
its "Video preview" label (Library), never a broken image. History's cover URL names
|
||||
no format, so the server serves a `.jxl` preview only when the request's `Accept`
|
||||
lists `image/jxl`; otherwise the file's preview in another format, else the original
|
||||
no format, so the server serves a `.jxl` preview only when the URL carries `jxl=1`
|
||||
(added by the workspace where its probe decoded JPEG XL: Firefox may not list it in
|
||||
Accept; any value but `0` or `1` is a 400) or the request's `Accept` lists
|
||||
`image/jxl`; otherwise the file's preview in another format, else the original
|
||||
image or a video's placeholder. Appearance says when this browser cannot show JPEG XL thumbnails.
|
||||
|
||||
Overview (`system-overview.mjs`) reads `GET /prompt_manager/system/overview` once per
|
||||
|
||||
+13
-1
@@ -143,6 +143,18 @@ def _query(request: web.Request) -> None:
|
||||
raise ValueError("Duplicate history query parameter")
|
||||
|
||||
|
||||
def _jxl_flag(request: web.Request) -> bool:
|
||||
"""The cover's ``jxl`` flag (#317): ``1`` when the workspace decoded a
|
||||
JPEG XL image (a browser may not list ``image/jxl`` in Accept), ``0`` or
|
||||
absent otherwise; anything else is a bad request."""
|
||||
value = request.query.get("jxl")
|
||||
if value is None:
|
||||
return False
|
||||
if value not in ("0", "1"):
|
||||
raise ValueError("Invalid jxl flag")
|
||||
return value == "1"
|
||||
|
||||
|
||||
def _page_limit(request: web.Request) -> int:
|
||||
limit = _decimal(request.query.get("limit", "40"), minimum=1)
|
||||
if limit > 100:
|
||||
@@ -795,7 +807,7 @@ class HistoryRoutesMixin:
|
||||
media_options = {
|
||||
"ensure_preview": lambda *preview: needed.append(preview),
|
||||
"placeholder": self._video_placeholder_bytes(),
|
||||
"accepts_jxl": accepts_jxl(request),
|
||||
"accepts_jxl": _jxl_flag(request) or accepts_jxl(request),
|
||||
}
|
||||
prepared = await execute()
|
||||
if needed:
|
||||
|
||||
@@ -40,3 +40,22 @@ test('stale inspection echoes use the canonical recovery vocabulary and identity
|
||||
}
|
||||
});
|
||||
test('the recorded output inspector cover is its preview, not the original (#249)',()=>{const a=adapter(),target=media('2',1);const record=a.toRecord(envelope(target),target,extractGenerationFacts);assert.deepEqual(record.preview,{url:target.thumbnail_url});assert.notEqual(record.preview.url,record.download.url);});
|
||||
|
||||
// #317 M3 follow-up: Firefox may decode JPEG XL without listing image/jxl in Accept, so the cover URL says so itself.
|
||||
test('the recorded output cover asks for JPEG XL only where this browser decodes it',async()=>{
|
||||
const support=await import('../../web/js/workspace/thumbnail-support.mjs');
|
||||
const a=adapter(),target=media('2',1);
|
||||
const probe=ok=>({createImage:()=>({set src(_){queueMicrotask(()=>ok?this.onload():this.onerror());}})});
|
||||
try {
|
||||
support.resetJxlSupport();
|
||||
assert.equal(a.toRecord(envelope(target),target,extractGenerationFacts).preview.url,target.thumbnail_url,'unknown: no flag');
|
||||
await support.detectJxlSupport(probe(true));
|
||||
const flagged=new URL(a.toRecord(envelope(target),target,extractGenerationFacts).preview.url,'http://localhost/');
|
||||
assert.equal(flagged.searchParams.get('jxl'),'1');
|
||||
assert.equal(flagged.searchParams.getAll('jxl').length,1);
|
||||
assert.equal(`${flagged.pathname}?${new URLSearchParams([...flagged.searchParams].filter(([k])=>k!=='jxl'))}`,target.thumbnail_url);
|
||||
support.resetJxlSupport();
|
||||
await support.detectJxlSupport(probe(false));
|
||||
assert.equal(a.toRecord(envelope(target),target,extractGenerationFacts).preview.url,target.thumbnail_url,'no support: no flag');
|
||||
} finally {support.resetJxlSupport();}
|
||||
});
|
||||
|
||||
@@ -1238,6 +1238,36 @@ class HistoryOutputAPITests(ImageAPITestCase):
|
||||
self.assertEqual(response.headers["Content-Type"], "image/jxl")
|
||||
self.assertEqual(body, jxl.read_bytes())
|
||||
|
||||
async def test_the_jxl_flag_selects_the_jxl_cover_without_accept(self):
|
||||
# M3 follow-up: a browser that decodes JPEG XL but does not list it in
|
||||
# Accept (Firefox) says so with jxl=1 on the cover URL.
|
||||
make_png(self.output_dir / "image.png")
|
||||
folder = self.output_dir / "thumbnails"
|
||||
folder.mkdir(exist_ok=True)
|
||||
jxl = folder / "image_thumb.jxl"
|
||||
jxl.write_bytes(b"\xff\x0a jxl preview")
|
||||
start = self.capture([self.ref()])
|
||||
descriptor = await self.media(start)
|
||||
from py.api import thumbnail_encode
|
||||
from py.config import PromptManagerConfig
|
||||
|
||||
with (
|
||||
patch.object(PromptManagerConfig, "THUMBNAIL_FORMAT", "jxl"),
|
||||
patch.object(thumbnail_encode, "detect_jxl_encoder", return_value="cjxl"),
|
||||
):
|
||||
url = descriptor["thumbnail_url"]
|
||||
response = await self.current(url + "&jxl=1", headers={"Accept": "*/*"})
|
||||
self.assertEqual(response.headers["Content-Type"], "image/jxl")
|
||||
self.assertEqual(response.headers["Cache-Control"], "no-store")
|
||||
self.assertEqual(await response.read(), jxl.read_bytes())
|
||||
response = await self.current(url + "&jxl=0", headers={"Accept": "*/*"})
|
||||
self.assertEqual(response.headers["Content-Type"], "image/png")
|
||||
await response.read()
|
||||
for bad in ("&jxl=true", "&jxl=", "&jxl=1&jxl=1", "&jxl=2"):
|
||||
response = await self.current(url + bad)
|
||||
self.assertEqual(response.status, 400, bad)
|
||||
await response.read()
|
||||
|
||||
async def test_a_video_jxl_cover_is_the_placeholder_without_jxl_support(self):
|
||||
(self.output_dir / "clip.webm").write_bytes(b"AV1 stream no decoder here")
|
||||
response, body, _jxl = await self._jxl_cover("clip.webm", "image/*,*/*")
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {projectInspectionPayload} from './inspection-state.mjs';
|
||||
import {jxlSupport} from './thumbnail-support.mjs';
|
||||
|
||||
const object = value => value !== null && typeof value === 'object' && !Array.isArray(value);
|
||||
const uuid = value => typeof value === 'string' && /^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/.test(value);
|
||||
@@ -65,8 +66,10 @@ export function toHistoryInspectionRecord(result, target, extractFacts) {
|
||||
const identity = {kind:'reported-location', key:target.location_key, path:target.reference.path_style === 'windows' ? path.replaceAll('\\', '/') : path,
|
||||
occurrence:{...target.occurrence}, history_epoch:target.history_epoch, current_scope:target.current_scope, reference:{...target.reference}};
|
||||
const record = projectInspectionPayload(result, identity, {url:urlFor(target, 'url', 'media'), filename:target.filename}, extractFacts);
|
||||
// #249: the inspector's cover is the recorded output's preview (made by the server on first view).
|
||||
record.preview = {url:urlFor(target, 'thumbnail_url', 'thumbnail')};
|
||||
// #249: the inspector's cover is the recorded output's preview (made by the server on first view). #317: its URL
|
||||
// names no format, so where this browser decoded JPEG XL it says so (`jxl=1`); the server also reads Accept.
|
||||
const cover = urlFor(target, 'thumbnail_url', 'thumbnail');
|
||||
record.preview = {url:jxlSupport() === true ? `${cover}&jxl=1` : cover};
|
||||
return record;
|
||||
}
|
||||
export const reportedLocationAdapter = Object.freeze({
|
||||
|
||||
Reference in New Issue
Block a user