On a drop-in host's dev server a missing posters/<id>.webp answered 200 text/html (the app's own page), and export handed that page to the print dialog as the image. The .html source path had the same hole: the rasteriser lifts the first <svg> on whatever page it is given. library/packet/poster-fetch.js now fetches both. A raster counts only if it is an image: by content-type, or by its first bytes when a host labels a WebP application/octet-stream. A source counts only if it is a poster document (its class="stage" element); a 200 that is not one reports 404. A thrown fetch on the raster stays a miss, so the source fallback still runs; a thrown fetch on the source is still reported as itself. Measured against a real local HTTP server with a single-page-app fallback: real WebP accepted (image/webp and octet-stream), missing WebP refused (200 fallback page and 404), real source accepted, missing source 404 (200 fallback page and 404), unreachable host a miss for the raster and "fetch failed" for the source. usePosterPrints uses it. PosterWall follows in its own commit.
45 lines
2.5 KiB
JavaScript
45 lines
2.5 KiB
JavaScript
// poster-fetch.js — fetch a poster's raster or its source, and refuse anything else.
|
|
//
|
|
// ★ WHY. A dev server or a single-page-app host answers a MISSING file with its own page:
|
|
// 200, text/html. Measured on a drop-in host's dev server (Nuxt): a missing
|
|
// posters/<id>.webp answered 200 text/html. Taken at face value that page became the
|
|
// "poster": as a raster, a broken image handed to the print dialog; as a source, the
|
|
// rasteriser lifts the first <svg> it finds (an icon on the host's page) and prints that.
|
|
// So a raster counts only if it is an image, and a source only if it is a poster
|
|
// document (the `class="stage"` element every generated poster source has).
|
|
//
|
|
// One helper for every caller: usePosterPrints and PosterWall each carried their own copy
|
|
// of this fetch, and a guard added to one did not reach the other.
|
|
|
|
export const POSTER_SOURCE_MARK = 'class="stage"'
|
|
|
|
// The image's first bytes, for hosts that label a WebP as application/octet-stream.
|
|
async function looksLikeImage(blob) {
|
|
const b = new Uint8Array(await blob.slice(0, 12).arrayBuffer())
|
|
const ascii = (from, to) => String.fromCharCode(...b.slice(from, to))
|
|
return (ascii(0, 4) === 'RIFF' && ascii(8, 12) === 'WEBP') // WebP
|
|
|| (b[0] === 0x89 && ascii(1, 4) === 'PNG') // PNG
|
|
|| (b[0] === 0xff && b[1] === 0xd8 && b[2] === 0xff) // JPEG
|
|
}
|
|
|
|
// The raster at `url` as a Blob, or null when it is missing, unreachable, or not an image.
|
|
export async function fetchPosterRaster(url, fetchImpl = fetch) {
|
|
let res
|
|
try { res = await fetchImpl(url) } catch { return null }
|
|
if (!res || !res.ok) return null
|
|
const type = (res.headers.get('content-type') || '').toLowerCase()
|
|
if (type.startsWith('text/')) return null
|
|
const blob = await res.blob()
|
|
return type.startsWith('image/') || await looksLikeImage(blob) ? blob : null
|
|
}
|
|
|
|
// The poster source at `url`: { html } when it is one, else { html: null, status }. A 200 that
|
|
// is not a poster document reports 404, since the poster is not there. A network error is
|
|
// thrown, not reported as a status: the source is the last resort, so the reader should see
|
|
// the real failure rather than "not found".
|
|
export async function fetchPosterSource(url, fetchImpl = fetch) {
|
|
const res = await fetchImpl(url)
|
|
if (!res.ok) return { html: null, status: res.status }
|
|
const html = await res.text()
|
|
return html.includes(POSTER_SOURCE_MARK) ? { html, status: res.status } : { html: null, status: 404 }
|
|
}
|