Parcourir la source

Photo app compression feature

- Added photo app compress file serve feature  to reduce load time for large photo files
Toby Chui il y a 1 semaine
Parent
commit
f105a00b3c

+ 88 - 0
src/mod/agi/agi.image.go

@@ -2,6 +2,7 @@ package agi
 
 import (
 	"bytes"
+	"encoding/base64"
 	"encoding/json"
 	"errors"
 	"fmt"
@@ -233,6 +234,92 @@ func (g *Gateway) injectImageLibFunctions(payload *static.AgiLibInjectionPayload
 		return otto.TrueValue()
 	})
 
+	//Resize image and return as base64 data URL, require (filepath, width, height, format)
+	vm.Set("_imagelib_resizeImageBase64", func(call otto.FunctionCall) otto.Value {
+		vsrc, err := call.Argument(0).ToString()
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+
+		width, err := call.Argument(1).ToInteger()
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+
+		height, err := call.Argument(2).ToInteger()
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+
+		format := "jpeg"
+		if !call.Argument(3).IsUndefined() {
+			format, err = call.Argument(3).ToString()
+			if err != nil {
+				format = "jpeg"
+			}
+		}
+
+		//Convert the virtual path to real path
+		srcfsh, rsrc, err := static.VirtualPathToRealPath(vsrc, u)
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+
+		resizeOpeningFile := rsrc
+		var srcFile arozfs.File
+		if srcfsh.RequireBuffer {
+			resizeOpeningFile, _, err = g.bufferRemoteResourcesToLocal(srcfsh, u, rsrc)
+			if err != nil {
+				g.RaiseError(err)
+				return otto.FalseValue()
+			}
+
+			srcFile, err = os.Open(resizeOpeningFile)
+			if err != nil {
+				g.RaiseError(err)
+				return otto.FalseValue()
+			}
+		} else {
+			srcFile, err = srcfsh.FileSystemAbstraction.Open(resizeOpeningFile)
+			if err != nil {
+				g.RaiseError(err)
+				return otto.FalseValue()
+			}
+		}
+		defer srcFile.Close()
+
+		//Decode and resize the image
+		src, err := imaging.Decode(srcFile)
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+		src = imaging.Resize(src, int(width), int(height), imaging.Lanczos)
+
+		//Encode to bytes buffer
+		var buf bytes.Buffer
+		if format == "png" {
+			err = png.Encode(&buf, src)
+		} else {
+			err = jpeg.Encode(&buf, src, &jpeg.Options{Quality: 85})
+		}
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+
+		//Convert to base64
+		imageBytes := buf.Bytes()
+		base64String := "data:image/" + format + ";base64," + base64.StdEncoding.EncodeToString(imageBytes)
+
+		result, _ := vm.ToValue(base64String)
+		return result
+	})
+
 	//Crop the given image, require (input, output, posx, posy, width, height)
 	vm.Set("_imagelib_cropImage", func(call otto.FunctionCall) otto.Value {
 		vsrc, err := call.Argument(0).ToString()
@@ -516,6 +603,7 @@ func (g *Gateway) injectImageLibFunctions(payload *static.AgiLibInjectionPayload
 		var imagelib = {};
 		imagelib.getImageDimension = _imagelib_getImageDimension;
 		imagelib.resizeImage = _imagelib_resizeImage;
+		imagelib.resizeImageBase64 = _imagelib_resizeImageBase64;
 		imagelib.cropImage = _imagelib_cropImage;
 		imagelib.loadThumbString = _imagelib_loadThumbString;
 		imagelib.hasExif = _imagelib_hasExif;

+ 77 - 0
src/web/Photo/backend/getCompressedImg.js

@@ -0,0 +1,77 @@
+/*
+    Compressed Image Retrieval Module
+    Retrieves compressed images for display in the Photo application.
+    
+    This module compresses images to a maximum of 2048x2048 pixels
+    while maintaining aspect ratio and returns as base64 data URL.
+*/
+
+requirelib("imagelib");
+
+function main() {
+    // Get the filepath from the request
+    if (typeof(filepath) == "undefined") {
+        sendJSONResp(JSON.stringify({
+            error: "filepath parameter is required"
+        }));
+        return;
+    }
+
+    // Define max dimensions
+    var maxWidth = 1024;
+    var maxHeight = 1024;
+
+    // Get original image dimensions
+    var imgInfo = imagelib.getImageDimension(filepath);
+    if (!imgInfo || !imgInfo[0] || !imgInfo[1]) {
+        // If we can't get dimensions, return error
+        sendJSONResp(JSON.stringify({
+            error: "Cannot read image dimensions"
+        }));
+        return;
+    }
+
+    var originalWidth = imgInfo[0];
+    var originalHeight = imgInfo[1];
+
+    // Calculate new dimensions while maintaining aspect ratio
+    var newWidth = originalWidth;
+    var newHeight = originalHeight;
+
+    if (originalWidth > maxWidth || originalHeight > maxHeight) {
+        var widthRatio = maxWidth / originalWidth;
+        var heightRatio = maxHeight / originalHeight;
+        var ratio = Math.min(widthRatio, heightRatio);
+
+        newWidth = Math.round(originalWidth * ratio);
+        newHeight = Math.round(originalHeight * ratio);
+    }
+
+    // Determine output format from file extension
+    var ext = filepath.split(".").pop().toLowerCase();
+    var format = "jpeg";
+    if (ext == "png") {
+        format = "png";
+    }
+
+    // Resize the image and get base64 data URL
+    try {
+        var base64DataUrl = imagelib.resizeImageBase64(filepath, newWidth, newHeight, format);
+        
+        if (base64DataUrl) {
+            // Return the base64 data URL as plain text
+            sendResp(base64DataUrl);
+        } else {
+            sendJSONResp(JSON.stringify({
+                error: "Failed to resize image"
+            }));
+        }
+    } catch (error) {
+        sendJSONResp(JSON.stringify({
+            error: "Failed to compress image: " + error.toString()
+        }));
+    }
+}
+
+main();
+

+ 12 - 1
src/web/Photo/backend/listFolder.js

@@ -121,7 +121,18 @@ function main(){
     // Filter out JPG duplicates when RAW files exist
     files = filterDuplicates(files);
 
-    sendJSONResp(JSON.stringify([folders, files]));
+    // Add filesize information to each file
+    var filesWithSize = [];
+    for (var i = 0; i < files.length; i++){
+        var filepath = files[i];
+        var filesize = filelib.filesize(filepath);
+        filesWithSize.push({
+            filepath: filepath,
+            filesize: filesize
+        });
+    }
+
+    sendJSONResp(JSON.stringify([folders, filesWithSize]));
 }
 
 main();

+ 180 - 80
src/web/Photo/index.html

@@ -148,6 +148,7 @@
             padding: 20px;
             overflow: hidden;
             cursor: grab;
+            position: relative;
         }
 
         .viewer-left img {
@@ -159,6 +160,21 @@
             user-select: none;
         }
 
+        #compressedImage {
+            position: absolute;
+            top: 50%;
+            left: 50%;
+            transform: translate(-50%, -50%);
+            opacity: 1;
+            transition: opacity 0.3s ease-out;
+            z-index: 2;
+        }
+
+        #compressedImage.hidden {
+            opacity: 0;
+            pointer-events: none;
+        }
+
         .viewer-left img.zoomed {
             max-width: none;
             max-height: none;
@@ -301,37 +317,94 @@
             }
         }
 
-        /* Zoom Snackbar Styles */
-        .zoom-snackbar {
+        /* Zoom Controls Styles */
+        .zoom-controls {
             position: absolute;
             bottom: 20px;
             left: 20px;
-            background: rgba(0, 0, 0, 0.8);
+            display: flex;
+            flex-direction: column;
+            gap: 8px;
+            z-index: 10;
+        }
+
+        .zoom-btn {
+            background: rgba(0, 0, 0, 0.7);
             color: white;
-            padding: 8px 12px;
+            border: 1px solid rgba(255, 255, 255, 0.2);
+            width: 40px;
+            height: 40px;
             border-radius: 4px;
+            cursor: pointer;
+            font-size: 20px;
+            font-weight: bold;
+            transition: all 0.2s;
             display: flex;
             align-items: center;
-            gap: 10px;
-            font-size: 14px;
-            font-family: Arial, sans-serif;
-            z-index: 1001;
+            justify-content: center;
             backdrop-filter: blur(4px);
         }
 
-        .zoom-reset-btn {
-            background: #f76c5d;
+        .zoom-btn:hover {
+            background: rgba(247, 108, 93, 0.9);
+            border-color: #f76c5d;
+        }
+
+        .zoom-btn:active {
+            transform: scale(0.95);
+        }
+
+        .zoom-level-indicator {
+            background: rgba(0, 0, 0, 0.7);
             color: white;
-            border: none;
-            padding: 4px 8px;
-            border-radius: 3px;
-            cursor: pointer;
+            padding: 6px 10px;
+            border-radius: 4px;
             font-size: 12px;
-            transition: background-color 0.2s;
+            text-align: center;
+            backdrop-filter: blur(4px);
+            border: 1px solid rgba(255, 255, 255, 0.2);
         }
 
-        .zoom-reset-btn:hover {
-            background: #e55a4f;
+        /* Navigation Controls Styles */
+        .nav-controls {
+            position: absolute;
+            bottom: 20px;
+            right: 20px;
+            display: flex;
+            flex-direction: row;
+            gap: 8px;
+            z-index: 10;
+        }
+
+        .nav-btn {
+            background: rgba(0, 0, 0, 0.7);
+            color: white;
+            border: 1px solid rgba(255, 255, 255, 0.2);
+            width: 40px;
+            height: 40px;
+            border-radius: 4px;
+            cursor: pointer;
+            font-size: 20px;
+            font-weight: bold;
+            transition: all 0.2s;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            backdrop-filter: blur(4px);
+        }
+
+        .nav-btn:hover:not(:disabled) {
+            background: rgba(247, 108, 93, 0.9);
+            border-color: #f76c5d;
+        }
+
+        .nav-btn:active:not(:disabled) {
+            transform: scale(0.95);
+        }
+
+        .nav-btn:disabled {
+            opacity: 0.3;
+            cursor: not-allowed;
         }
 
         @media (max-width: 768px) {
@@ -339,6 +412,23 @@
                 display: none !important;
             }
         }
+
+        /* Loading Progress Indicator */
+        .loading-progress {
+            position: absolute;
+            bottom: 20px;
+            left: 50%;
+            transform: translateX(-50%);
+            background: rgba(0, 0, 0, 0.7);
+            color: white;
+            padding: 8px 16px;
+            border-radius: 4px;
+            font-size: 14px;
+            backdrop-filter: blur(4px);
+            border: 1px solid rgba(255, 255, 255, 0.2);
+            z-index: 10;
+            display: none;
+        }
     </style>
 
 </head>
@@ -410,21 +500,21 @@
             <div id="viewboxContainer">
                 <div x-show="viewMode === 'grid'" id="viewbox" class="ui six cards viewbox">
                     <template x-for="image in images">
-                        <div class="imagecard" style="cursor: pointer;" x-on:click="showImage($el); ShowModal();" :style="{width: renderSize + 'px', height: renderSize + 'px'}" :filedata="encodeURIComponent(JSON.stringify({'filename':image.split('/').pop(),'filepath':image}))">
+                        <div class="imagecard" style="cursor: pointer;" x-on:click="showImage($el); ShowModal();" :style="{width: renderSize + 'px', height: renderSize + 'px'}" :filedata="encodeURIComponent(JSON.stringify({'filename':image.filepath.split('/').pop(),'filepath':image.filepath,'filesize':image.filesize}))">
                             <a class="image" x-init="updateImageSizes();">
-                                <img :src="'../system/file_system/loadThumbnail?bytes=true&vpath=' + image">
+                                <img :src="'../system/file_system/loadThumbnail?bytes=true&vpath=' + image.filepath">
                             </a>
                         </div>
                     </template>
                 </div>
                 <div x-show="viewMode === 'list'" class="ui relaxed divided inverted list">
                     <template x-for="image in images">
-                        <div class="item" style="cursor: pointer; padding-left: 10px; " x-on:click="showImage($el); ShowModal();" :filedata="encodeURIComponent(JSON.stringify({'filename':image.split('/').pop(),'filepath':image}))">
-                            <img class="ui small image" :src="'../system/file_system/loadThumbnail?bytes=true&vpath=' + image"
+                        <div class="item" style="cursor: pointer; padding-left: 10px; " x-on:click="showImage($el); ShowModal();" :filedata="encodeURIComponent(JSON.stringify({'filename':image.filepath.split('/').pop(),'filepath':image.filepath,'filesize':image.filesize}))">
+                            <img class="ui small image" :src="'../system/file_system/loadThumbnail?bytes=true&vpath=' + image.filepath"
                                  style="width: 60px; height: 60px;">
                             <div class="content">
-                                <div class="header" x-text="image.split('/').pop()"></div>
-                                <div class="description" x-text="image"></div>
+                                <div class="header" x-text="image.filepath.split('/').pop()"></div>
+                                <div class="description" x-text="image.filepath"></div>
                             </div>
                         </div>
                     </template>
@@ -442,9 +532,26 @@
                 <img id="bg-image" src="" />
             </div>
             <div class="viewer-left">
+                <img id="compressedImage" src="" style="display: none;" />
                 <img id="fullImage" src="img/loading.png" />
                 <button class="close-btn" onclick="closeViewer()">×</button>
                 <button class="show-info-btn" onclick="showInfoPanel()">ℹ</button>
+                
+                <!-- Loading Progress -->
+                <div id="loading-progress" class="loading-progress">Loading 0%</div>
+                
+                <!-- Zoom Controls -->
+                <div id="zoom-controls" class="zoom-controls">
+                    <button class="zoom-btn" onclick="zoomIn()" title="Zoom In">+</button>
+                    <div class="zoom-level-indicator" id="zoom-level-display">100%</div>
+                    <button class="zoom-btn" onclick="zoomOut()" title="Zoom Out">−</button>
+                </div>
+                
+                <!-- Navigation Controls -->
+                <div id="nav-controls" class="nav-controls">
+                    <button id="prev-btn" class="nav-btn" onclick="showPreviousImage()" title="Previous Photo">‹</button>
+                    <button id="next-btn" class="nav-btn" onclick="showNextImage()" title="Next Photo">›</button>
+                </div>
             </div>
             <div class="viewer-right">
                 <div>
@@ -590,12 +697,6 @@
                 </div>
             </div>
         </div>
-        
-        <!-- Zoom Snackbar -->
-        <div id="zoom-snackbar" class="zoom-snackbar" style="display: none;">
-            <span id="zoom-level-display">x1.0</span>
-            <button id="zoom-reset-btn" class="zoom-reset-btn" onclick="resetZoom()">Reset</button>
-        </div>
     </div>
 
 </body>
@@ -622,6 +723,31 @@
         document.querySelector('.viewer-right').style.display = 'block';
     }
 
+    // Navigation functionality
+    function showPreviousImage() {
+        if (typeof prePhoto !== 'undefined' && prePhoto != null) {
+            showImage(prePhoto);
+        }
+    }
+
+    function showNextImage() {
+        if (typeof nextPhoto !== 'undefined' && nextPhoto != null) {
+            showImage(nextPhoto);
+        }
+    }
+
+    function updateNavigationButtons() {
+        const prevBtn = document.getElementById('prev-btn');
+        const nextBtn = document.getElementById('next-btn');
+        
+        if (prevBtn) {
+            prevBtn.disabled = (typeof prePhoto === 'undefined' || prePhoto == null || !$(prePhoto).hasClass("imagecard"));
+        }
+        if (nextBtn) {
+            nextBtn.disabled = (typeof nextPhoto === 'undefined' || nextPhoto == null || !$(nextPhoto).hasClass("imagecard"));
+        }
+    }
+
     // Zoom and Pan functionality
     let zoomLevel = 1;
     let panX = 0;
@@ -642,25 +768,40 @@
 
     function updateImageTransform() {
         const img = document.getElementById('fullImage');
-        const snackbar = document.getElementById('zoom-snackbar');
+        const controls = document.getElementById('zoom-controls');
         const zoomDisplay = document.getElementById('zoom-level-display');
         
         if (zoomLevel > 1) {
             img.style.transform = `scale(${zoomLevel}) translate(${panX}px, ${panY}px)`;
             img.classList.add('zoomed');
-            
-            // Show zoom snackbar
-            zoomDisplay.textContent = 'x' + zoomLevel.toFixed(1);
-            snackbar.style.display = 'flex';
         } else {
             img.style.transform = 'none';
             img.classList.remove('zoomed');
             panX = 0;
             panY = 0;
-            
-            // Hide zoom snackbar
-            snackbar.style.display = 'none';
         }
+        
+        // Always show zoom controls and update display
+        zoomDisplay.textContent = Math.round(zoomLevel * 100) + '%';
+        controls.style.display = 'flex';
+    }
+
+    function zoomIn() {
+        const img = document.getElementById('fullImage');
+        const rect = img.getBoundingClientRect();
+        const centerX = rect.left + rect.width / 2;
+        const centerY = rect.top + rect.height / 2;
+        let newZoom = Math.min(3, zoomLevel + 0.2);
+        zoomAtPoint(newZoom, centerX, centerY);
+    }
+
+    function zoomOut() {
+        const img = document.getElementById('fullImage');
+        const rect = img.getBoundingClientRect();
+        const centerX = rect.left + rect.width / 2;
+        const centerY = rect.top + rect.height / 2;
+        let newZoom = Math.max(1, zoomLevel - 0.2);
+        zoomAtPoint(newZoom, centerX, centerY);
     }
 
     function zoomAtPoint(scale, centerX, centerY) {
@@ -707,26 +848,6 @@
         // Reset zoom state
         resetZoom();
 
-        // Define handler functions to allow removal and re-addition
-        let handleDblClick = function(e) {
-            console.log(e);
-            e.preventDefault();
-            if (zoomLevel > 1) {
-                resetZoom();
-            } else {
-                zoomAtPoint(2, e.clientX, e.clientY);
-            }
-        };
-
-        let handleWheel = function(e) {
-            e.preventDefault();
-            const delta = e.deltaY > 0 ? 0.9 : 1.1;
-            let actualZoomLevel = zoomLevel * delta;
-            if (actualZoomLevel < 1) actualZoomLevel = 1;
-            if (actualZoomLevel > 3.3) actualZoomLevel = 3.3;
-            zoomAtPoint(actualZoomLevel, e.clientX, e.clientY);
-        };
-
         let handleMouseDown = function(e) {
             if (zoomLevel > 1) {
                 e.preventDefault();
@@ -830,8 +951,6 @@
         };
 
         // Remove existing listeners to prevent double triggering
-        img.removeEventListener('dblclick', handleDblClick);
-        img.removeEventListener('wheel', handleWheel);
         img.removeEventListener('mousedown', handleMouseDown);
         document.removeEventListener('mousemove', handleMouseMove);
         document.removeEventListener('mouseup', handleMouseUp);
@@ -840,8 +959,6 @@
         img.removeEventListener('touchend', handleTouchEnd);
 
         // Add listeners back
-        img.addEventListener('dblclick', handleDblClick);
-        img.addEventListener('wheel', handleWheel);
         img.addEventListener('mousedown', handleMouseDown);
         document.addEventListener('mousemove', handleMouseMove);
         document.addEventListener('mouseup', handleMouseUp);
@@ -849,26 +966,9 @@
         img.addEventListener('touchmove', handleTouchMove);
         img.addEventListener('touchend', handleTouchEnd);
         
-        // Double click to zoom
-        img.addEventListener('dblclick', function(e) {
-            console.log(e);
-            e.preventDefault();
-            if (zoomLevel > 1) {
-                resetZoom();
-            } else {
-                zoomAtPoint(2, e.clientX, e.clientY);
-            }
-        });
+        // Double-click zoom removed - use +/- buttons instead
         
-        // Mouse wheel zoom
-        img.addEventListener('wheel', function(e) {
-            e.preventDefault();
-            const delta = e.deltaY > 0 ? 0.9 : 1.1;
-            let actualZoomLevel = zoomLevel * delta;
-            if (actualZoomLevel < 1) actualZoomLevel = 1;
-            if (actualZoomLevel > 3.3) actualZoomLevel = 3.3;;
-            zoomAtPoint(actualZoomLevel, e.clientX, e.clientY);
-        });
+        // Mouse wheel zoom removed - use +/- buttons instead
         
         // Mouse drag pan
         img.addEventListener('mousedown', function(e) {

+ 189 - 20
src/web/Photo/photo.js

@@ -11,6 +11,14 @@ let prePhoto = "";
 let nextPhoto = "";
 let currentModel = "";
 
+// Check if image should use compression (only JPG/PNG)
+function shouldUseCompression(filepath, filesize) {
+    const ext = filepath.split('.').pop().toLowerCase();
+    const isJpgOrPng = (ext === 'jpg' || ext === 'jpeg' || ext === 'png');
+    const COMPRESSION_THRESHOLD = 5 * 1024 * 1024; // 5MB
+    return isJpgOrPng && filesize && filesize > COMPRESSION_THRESHOLD;
+}
+
 // Get viewable image URL (handles RAW files)
 function getViewableImageUrl(filepath, callback) {
     // Both RAW and regular images now use backend rendering
@@ -212,6 +220,7 @@ function closeViewer(){
 
     setTimeout(function(){
         $("#fullImage").attr("src","img/loading.png");
+        $("#compressedImage").attr("src","").hide().removeClass('hidden');
         $("#bg-image").attr("src","");
         $("#info-filename").text("");
         $("#info-filepath").text("");
@@ -243,33 +252,83 @@ function showImage(object){
         resetZoom();
     }
 
+    if (!$(object).hasClass("imagecard")){
+        // Not an image card, do nothing
+        return;
+    }
     var fd = JSON.parse(decodeURIComponent($(object).attr("filedata")));
-
-    // Update image dimensions and generate histogram when loaded
-    $("#fullImage").off("load").on('load', function() {
-        let width = this.naturalWidth;
-        let height = this.naturalHeight;
-        $("#info-dimensions").text(width + ' × ' + height + "px");
-
-        // Wait for image to be ready, then generate histogram
-        const canvas = document.getElementById('histogram-canvas');
-        if (canvas) {
-            generateHistogram(document.getElementById('fullImage'), canvas);
-        }
-    });
+    $("#info-dimensions").text("Calculating...");
+    // Check if we should use compression (only for JPG/PNG > 5MB)
+    const useCompression = shouldUseCompression(fd.filepath, fd.filesize);
 
     // Get image URL (backend handles RAW files automatically)
     getViewableImageUrl(fd.filepath, (imageUrl, isSupported, isBlob, method) => {
-        $("#fullImage").attr('src', imageUrl);
-        $("#bg-image").attr('src', imageUrl);
+        const compressedImg = document.getElementById('compressedImage');
+        const fullImg = document.getElementById('fullImage');
+        const bgImg = document.getElementById('bg-image');
+        
+        // Reset compressed image
+        compressedImg.style.display = 'none';
+        compressedImg.classList.remove('hidden');
+         
+        if (useCompression) {
+            // Use compressed version for large JPG/PNG files
+            console.log('Large JPG/PNG detected (' + (fd.filesize / 1024 / 1024).toFixed(2) + 'MB), loading compressed version first');
+
+            fetch(ao_root + "system/ajgi/interface?script=Photo/backend/getCompressedImg.js", {
+                method: 'POST',
+                cache: 'no-cache',
+                headers: {
+                    'Content-Type': 'application/json'
+                },
+                body: JSON.stringify({
+                    "filepath": fd.filepath
+                })
+            }).then(resp => {
+                resp.text().then(dataURL => {
+                    // Show compressed image
+                    compressedImg.src = dataURL;
+                    compressedImg.style.display = 'block';
+                    bgImg.src = dataURL;
+
+                    // Start loading full-size image in background
+                    loadFullSizeImageInBackground(imageUrl, fd);
+                });
+            }).catch(error => {
+                console.error('Failed to load compressed image:', error);
+                // Fall back to full size image
+                fullImg.src = imageUrl;
+                bgImg.src = imageUrl;
+            });
+        } else {
+            $("#compressedImage").hide();
+            // Use full image URL directly for RAW, WEBP, or small JPG/PNG files
+            if (method === 'backend_raw') {
+                console.log('RAW file: Rendered by backend');
+            }
+            fullImg.src = imageUrl;
+            bgImg.src = imageUrl;
+        }
+
+        // Update image dimensions and generate histogram when full image loads
+        $("#fullImage").off("load").on('load', function() {
+            let width = this.naturalWidth;
+            let height = this.naturalHeight;
+            $("#info-dimensions").text(width + ' × ' + height + "px");
+
+            // Hide the compressed image once full image is loaded
+            $("#compressedImage").hide();
+
+            // Wait for image to be ready, then generate histogram
+            const canvas = document.getElementById('histogram-canvas');
+            if (canvas) {
+                generateHistogram(this, canvas);
+            }
+        });
+
         $("#info-filename").text(fd.filename);
         $("#info-filepath").text(fd.filepath);
 
-        // Log the rendering method used
-        if (method === 'backend_raw') {
-            console.log('RAW file: Rendered by backend');
-        }
-
         var nextCard = $(object).next();
         var prevCard = $(object).prev();
         if (nextCard.length > 0){
@@ -284,6 +343,11 @@ function showImage(object){
             prePhoto = null;
         }
 
+        // Update navigation buttons state
+        if (typeof updateNavigationButtons === 'function') {
+            updateNavigationButtons();
+        }
+
         ao_module_setWindowTitle("Photo - " + fd.filename);
 
         window.location.hash = encodeURIComponent(JSON.stringify({filename: fd.filename, filepath: fd.filepath}));
@@ -309,6 +373,111 @@ function showImage(object){
     });
 }
 
+
+// Function to load full-size image in background with progress tracking
+function loadFullSizeImageInBackground(fullSizeUrl, fileData) {
+    console.log('Starting background download of full-size image...');
+    
+  
+
+    const loadingIndicator = document.getElementById('loading-progress');
+    const fullImage = document.getElementById('fullImage');
+    const compressedImage = document.getElementById('compressedImage');
+    const bgImage = document.getElementById('bg-image');
+    fullImage.src = fullSizeUrl;
+
+    return;
+    // Legacy blob loading method
+    // Show loading indicator
+    /*
+    if (loadingIndicator) {
+        loadingIndicator.style.display = 'block';
+        loadingIndicator.textContent = 'Loading 0%';
+    }
+    
+    const xhr = new XMLHttpRequest();
+    xhr.open('GET', fullSizeUrl, true);
+    xhr.responseType = 'arraybuffer';
+    
+    // Track download progress
+    xhr.onprogress = function(event) {
+        if (event.lengthComputable) {
+            const percentComplete = (event.loaded / event.total) * 100;
+            console.log('Download progress: ' + percentComplete.toFixed(2) + '% (' + 
+                       (event.loaded / 1024 / 1024).toFixed(2) + 'MB / ' + 
+                       (event.total / 1024 / 1024).toFixed(2) + 'MB)');
+            
+            // Update loading indicator
+            if (loadingIndicator) {
+                loadingIndicator.textContent = 'Loading ' + Math.round(percentComplete) + '%';
+            }
+        } else {
+            console.log('Download progress: ' + (event.loaded / 1024 / 1024).toFixed(2) + 'MB downloaded');
+            
+            // Update loading indicator without percentage
+            if (loadingIndicator) {
+                loadingIndicator.textContent = 'Loading...';
+            }
+        }
+    };
+    
+    // Handle successful download
+    xhr.onload = function() {
+        // Hide loading indicator
+        if (loadingIndicator) {
+            loadingIndicator.style.display = 'none';
+        }
+        
+        if (xhr.status === 200) {
+            console.log('Full-size image downloaded successfully, swapping images...');
+            
+            // Set full image src directly (browser will cache it)
+            fullImage.onload = function() {
+                console.log('Full-size image loaded and cached');
+                
+                // Fade out compressed image
+                if (compressedImage.style.display !== 'none') {
+                    compressedImage.classList.add('hidden');
+                    setTimeout(() => {
+                        compressedImage.style.display = 'none';
+                    }, 300); // Match CSS transition duration
+                }
+                
+                // Update background
+                bgImage.src = fullSizeUrl;
+                
+                // Update dimensions with full-size image dimensions
+                $("#info-dimensions").text(fullImage.naturalWidth + ' × ' + fullImage.naturalHeight + "px");
+                
+                // Regenerate histogram with full-size image
+                const canvas = document.getElementById('histogram-canvas');
+                if (canvas) {
+                    generateHistogram(fullImage, canvas);
+                }
+            };
+            
+            // Set the source to trigger browser caching
+            fullImage.src = fullSizeUrl;
+        }
+    };
+    
+    // Handle errors
+    xhr.onerror = function() {
+        // Hide loading indicator
+        if (loadingIndicator) {
+            loadingIndicator.style.display = 'none';
+        }
+        console.error('Failed to download full-size image');
+        
+        // Try to load directly as fallback
+        fullImage.src = fullSizeUrl;
+    };
+    
+    // Start the download
+    xhr.send();
+    */
+}
+
 $(document).on("keydown", function(e){
     if (e.keyCode == 27){ // Escape
         if ($('#photo-viewer').is(':visible')) {