Revamped still-image

This commit is contained in:
Mergan 2023-09-12 02:48:53 -07:00
parent 174f98b1cb
commit e13c4b6b85
2 changed files with 75 additions and 30 deletions

View file

@ -39,27 +39,18 @@ const StillImage = {
this.imageLoadError && this.imageLoadError() this.imageLoadError && this.imageLoadError()
}, },
detectAnimation (image) { detectAnimation (image) {
// If there are no file extensions, the mimetype isn't set, and no mediaproxy is available, we can't figure out // It's a bit aggressive to assume all images we can't find the mimetype of is animated, but necessary for
// the mimetype of the image. // people in need of reduced motion accessibility. As such, we'll consider those images animated if the user
const hasFileExtension = this.src.split('/').pop().includes('.') // TODO: Better check? // agent is set to prefer reduced motion. Otherwise, it'll just be used as an early exit.
const mediaProxyAvailable = this.$store.state.instance.mediaProxyAvailable if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
if (!hasFileExtension && this.mimetype === undefined && !mediaProxyAvailable) { this.isAnimated = true
// It's a bit aggressive to assume all images we can't find the mimetype of is animated, but necessary for
// people in need of reduced motion accessibility. As such, we'll consider those images animated if the user
// agent is set to prefer reduced motion. Otherwise, it'll just be used as an early exit.
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches)
this.isAnimated = true
return
} }
if (this.mimetype === 'image/gif' || this.src.endsWith('.gif')) { const mediaProxyAvailable = this.$store.state.instance.mediaProxyAvailable
this.isAnimated = true
return
}
// harmless CORS errors without-- clean console with // harmless CORS errors without-- clean console with
if (!mediaProxyAvailable) return if (!mediaProxyAvailable) return
// Animated JPEGs?
if (!(this.src.endsWith('.webp') || this.src.endsWith('.png'))) return
// Browser Cache should ensure image doesn't get loaded twice if cache exists // Browser Cache should ensure image doesn't get loaded twice if cache exists
fetch(image.src, { fetch(image.src, {
referrerPolicy: 'same-origin' referrerPolicy: 'same-origin'
@ -68,11 +59,16 @@ const StillImage = {
// We don't need to read the whole file so only call it once // We don't need to read the whole file so only call it once
data.body.getReader().read() data.body.getReader().read()
.then(reader => { .then(reader => {
if (this.src.endsWith('.webp') && this.isAnimatedWEBP(reader.value)) { // Ordered from least to most intensive
if (this.isGIF(reader.value)) {
this.isAnimated = true this.isAnimated = true
return return
} }
if (this.src.endsWith('.png') && this.isAnimatedPNG(reader.value)) { if (this.isAnimatedWEBP(reader.value)) {
this.isAnimated = true
return
}
if (this.isAnimatedPNG(reader.value)) {
this.isAnimated = true this.isAnimated = true
} }
}) })
@ -81,6 +77,20 @@ const StillImage = {
// this.imageLoadError && this.imageLoadError() // this.imageLoadError && this.imageLoadError()
}) })
}, },
isGIF (data) {
// I am a perfectly sane individual
//
// GIF HEADER CHUNK
// === START HEADER ===
// 47 49 46 38 ("GIF8")
const gifHeader = [0x47, 0x49, 0x46];
for (let i = 0; i < 3; i++) {
if (data[i] !== gifHeader[i]) {
return false;
}
}
return true
},
isAnimatedWEBP (data) { isAnimatedWEBP (data) {
/** /**
* WEBP HEADER CHUNK * WEBP HEADER CHUNK
@ -114,16 +124,51 @@ const StillImage = {
const idatPos = str.indexOf('IDAT') const idatPos = str.indexOf('IDAT')
return (str.substring(0, idatPos > 0 ? idatPos : 0).indexOf('acTL') > 0) return (str.substring(0, idatPos > 0 ? idatPos : 0).indexOf('acTL') > 0)
}, },
drawThumbnail () { drawThumbnail() {
const canvas = this.$refs.canvas const canvas = this.$refs.canvas;
if (!this.$refs.canvas) return if (!canvas) return;
const image = this.$refs.src
const width = image.naturalWidth const context = canvas.getContext('2d');
const height = image.naturalHeight const image = this.$refs.src;
canvas.width = width const parentElement = canvas.parentElement;
canvas.height = height
canvas.getContext('2d').drawImage(image, 0, 0, width, height) // Draw the quick, unscaled version first
} context.drawImage(image, 0, 0, parentElement.clientWidth, parentElement.clientHeight);
// Use requestAnimationFrame to schedule the scaling to the next frame
requestAnimationFrame(() => {
// Adjust for high-DPI displays
const ratio = window.devicePixelRatio || 1;
canvas.width = parentElement.clientWidth * ratio;
canvas.height = parentElement.clientHeight * ratio;
canvas.style.width = `${parentElement.clientWidth}px`;
canvas.style.height = `${parentElement.clientHeight}px`;
context.scale(ratio, ratio);
// Maintain the aspect ratio of the image
const imgAspectRatio = image.naturalWidth / image.naturalHeight;
const canvasAspectRatio = parentElement.clientWidth / parentElement.clientHeight;
let drawWidth, drawHeight;
if (imgAspectRatio > canvasAspectRatio) {
drawWidth = parentElement.clientWidth;
drawHeight = parentElement.clientWidth / imgAspectRatio;
} else {
drawHeight = parentElement.clientHeight;
drawWidth = parentElement.clientHeight * imgAspectRatio;
}
context.clearRect(0, 0, canvas.width, canvas.height); // Clear the previous unscaled image
context.imageSmoothingEnabled = true;
context.imageSmoothingQuality = 'high';
// Draw the good one for realsies
const dx = (parentElement.clientWidth - drawWidth) / 2;
const dy = (parentElement.clientHeight - drawHeight) / 2;
context.drawImage(image, dx, dy, drawWidth, drawHeight);
});
}
}, },
updated () { updated () {
// On computed animated change // On computed animated change

View file

@ -60,7 +60,7 @@
&.animated { &.animated {
&::before { &::before {
zoom: var(--_still_image-label-scale, 1); zoom: var(--_still_image-label-scale, 1);
content: 'gif'; content: var(--image-type-label, 'A?');
position: absolute; position: absolute;
line-height: 1; line-height: 1;
font-size: 0.7em; font-size: 0.7em;