From e13c4b6b851df7b74ec18f42c0d100c5c57a6c95 Mon Sep 17 00:00:00 2001
From: Mergan <mergan@viridianpatriots.com>
Date: Tue, 12 Sep 2023 02:48:53 -0700
Subject: [PATCH] Revamped still-image

---
 src/components/still-image/still-image.js  | 103 +++++++++++++++------
 src/components/still-image/still-image.vue |   2 +-
 2 files changed, 75 insertions(+), 30 deletions(-)

diff --git a/src/components/still-image/still-image.js b/src/components/still-image/still-image.js
index 9c335d6c..963bb1ac 100644
--- a/src/components/still-image/still-image.js
+++ b/src/components/still-image/still-image.js
@@ -39,27 +39,18 @@ const StillImage = {
       this.imageLoadError && this.imageLoadError()
     },
     detectAnimation (image) {
-      // If there are no file extensions, the mimetype isn't set, and no mediaproxy is available, we can't figure out
-      // the mimetype of the image.
-      const hasFileExtension = this.src.split('/').pop().includes('.') // TODO: Better check?
-      const mediaProxyAvailable = this.$store.state.instance.mediaProxyAvailable
-      if (!hasFileExtension && this.mimetype === undefined && !mediaProxyAvailable) {
-        // 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
+      // 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
       }
 
-      if (this.mimetype === 'image/gif' || this.src.endsWith('.gif')) {
-        this.isAnimated = true
-        return
-      }
+      const mediaProxyAvailable = this.$store.state.instance.mediaProxyAvailable
+
       // harmless CORS errors without-- clean console with
       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
       fetch(image.src, {
         referrerPolicy: 'same-origin'
@@ -68,11 +59,16 @@ const StillImage = {
           // We don't need to read the whole file so only call it once
           data.body.getReader().read()
             .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
                 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
               }
             })
@@ -81,6 +77,20 @@ const StillImage = {
           // 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) {
       /**
        * WEBP HEADER CHUNK
@@ -114,16 +124,51 @@ const StillImage = {
       const idatPos = str.indexOf('IDAT')
       return (str.substring(0, idatPos > 0 ? idatPos : 0).indexOf('acTL') > 0)
     },
-    drawThumbnail () {
-      const canvas = this.$refs.canvas
-      if (!this.$refs.canvas) return
-      const image = this.$refs.src
-      const width = image.naturalWidth
-      const height = image.naturalHeight
-      canvas.width = width
-      canvas.height = height
-      canvas.getContext('2d').drawImage(image, 0, 0, width, height)
-    }
+    drawThumbnail() {
+      const canvas = this.$refs.canvas;
+      if (!canvas) return;
+    
+      const context = canvas.getContext('2d');
+      const image = this.$refs.src;
+      const parentElement = canvas.parentElement;
+    
+      // 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 () {
     // On computed animated change
diff --git a/src/components/still-image/still-image.vue b/src/components/still-image/still-image.vue
index fa3edacf..68e4ca49 100644
--- a/src/components/still-image/still-image.vue
+++ b/src/components/still-image/still-image.vue
@@ -60,7 +60,7 @@
   &.animated {
     &::before {
       zoom: var(--_still_image-label-scale, 1);
-      content: 'gif';
+      content: var(--image-type-label, 'A?');
       position: absolute;
       line-height: 1;
       font-size: 0.7em;