Spreadsheet Images

By default, uploaded images are stored as Base64 in the spreadsheet's JSON configuration, which inflates the document and its storage. Store them externally instead, for example in an S3 bucket, and replace the image source with a URL.

Intercepting the Upload

Server

Intercept uploads in the server's beforeChange event: upload the image to your storage (file system, S3) and replace the Base64 string with a URL before saving.

Client

Use the onbeforeloadimage event to load images from private sources such as S3 with the correct authentication.

Examples

Save images to the server's disk

Store images in a public folder and serve them as static files:

const server = require('@jspreadsheet/server');
const adapter = require('@jspreadsheet/server-redis');
const fs = require('fs');
const path = require('path');

// Helper function to save the image to disk
function saveImageToDisk(base64Image, imageName) {
    // Strip metadata from the base64 string
    const base64Data = base64Image.replace(/^data:image\/\w+;base64,/, '');
    // Define the directory where images will be saved
    const imageDir = path.join(__dirname, 'public', 'images');
    // Ensure the directory exists
    if (! fs.existsSync(imageDir)) {
        fs.mkdirSync(imageDir, { recursive: true });
    }
    // Full path for the image
    const imagePath = path.join(imageDir, imageName);
    // Write the image to the file system
    fs.writeFileSync(imagePath, base64Data, { encoding: 'base64' });
    // Return the URL of the image to be saved in the spreadsheet
    return `/images/${imageName}`;
}

// Jspreadsheet license
const license = {
    clientId: '356a192b7913b04c54574d18c28d46e6395428ab',
    licenseKey: 'YOUR_LICENSE_KEY'
};

// Create the Server
server({
    config: {
        cors: {
            origin: "*"
        },
    },
    port: 3000,
    error: function(e) {
        console.error(e);
    },
    beforeConnect: async function(auth) {
        return true;
    },
    load: async function(guid) {
        return await adapter.load(guid);
    },
    beforeChange: async function(guid, changes, query) {
        // Check for image upload event
        if (query && query.guid) {
            // Check for the correct event that upload images
            if (changes.method === 'setMedia' && Array.isArray(changes.args[0])) {
                // Go through all images
                for (const v of changes.args[0]) {
                    if (v.src && v.src.startsWith('data:')) {
                        // Generate a unique name for the image
                        const imageName = `image_${Date.now()}.png`;
                        // Save the image to disk
                        const imageUrl = saveImageToDisk(v.src, imageName);
                        // Replace the base64 src with the image URL
                        v.src = imageUrl;
                    }
                }
            }
        }

        return changes;
    },
    change: async function(guid, changes, query, onerror) {
        return await adapter.change(guid, changes, query, onerror);
    },
    create: async function(guid, config, query) {
        return await adapter.create(guid, config, query);
    },
    destroy: async function(guid, query) {
        return await adapter.destroy(guid, query);
    },
    license: license
});

Using S3 through the API extension

Configure the API extension with api({ s3 }), then call api.setImage(guid, base64) from beforeChange to upload the image and rewrite the source before it is persisted. setImage:

  • only accepts data:image/...;base64, URIs of type jpg, jpeg, gif, png or svg; anything else throws an error;
  • stores the decoded file in your bucket under the key <guid>/images/<uuid>.<extension>;
  • returns the path /api/<guid>/images/<uuid>.<extension>, which replaces the Base64 source in the document.

That path is served by GET /api/<guid>/images/<filename>, which streams the object from S3 through the server with the same authentication as loading the document, so the bucket can stay private. Without S3 configuration, the route responds with 500 S3 not defined.

Server-side Handling

const api = require('@jspreadsheet/server-api');
const server = require('@jspreadsheet/server');
const adapter = require('@jspreadsheet/server-mongodb');

// Jspreadsheet license
const license = {
    clientId: '356a192b7913b04c54574d18c28d46e6395428ab',
    licenseKey: 'YOUR_LICENSE_KEY'
}

// Define S3 configuration
api({
    s3: {
        key: "your-key",
        secret: "your-secret",
        bucket: "your-bucket",
        region: "us-your-region",
        url: "https://bucketname.s3.amazonaws.com/",
    }
})

server({
    config: {
        cors: {
            origin: "*"
        },
    },
    port: 3000,
    error: function(e) {
        console.error(e)
    },
    beforeConnect: async function(auth) {
        return true;
    },
    load: async function(guid) {
        return await adapter.load(guid);
    },
    beforeChange: async function(guid, changes, query) {
        if (query && query.guid) {
            if (changes.method === 'setMedia' && Array.isArray(changes.args[0])) {
                for (const v of changes.args[0]) {
                    if (v.src && v.src.startsWith('data:')) {
                        // Upload to S3
                        let ret = await api.setImage(guid, v.src);
                        if (ret) {
                            // Overwrite base64 with the image API path
                            v.src = ret;
                        }
                    }
                }
            }
        }

        return changes;
    },
    change: async function(guid, changes, query, onerror) {
        return await adapter.change(guid, changes, query, onerror);
    },
    create: async function(guid, config, query) {
        return await adapter.create(guid, config, query);
    },
    destroy: async function(guid, query) {
        return await adapter.destroy(guid, query);
    },
    license: license,
    extensions: { api },
});

Client-side Handling

In onbeforeloadimage, fetch images under the authenticated /api/<guid>/images/<filename> route with a Bearer token:

jspreadsheet(document.getElementById('spreadsheet'), {
    guid: guid,
    onbeforeloadimage: function (worksheet, img, options) {
        if (options.src.includes('/api')) {
            fetch('http://localhost:3000' + options.src, {
                headers: {
                    'Authorization': 'Bearer ' + token
                }
            })
            .then(response => response.blob())
            .then(blob => {
                img.src = URL.createObjectURL(blob);  // Set the image source to the blob URL
            });

            return false;  // Prevent default image load until the API call completes
        }
    },
});

Final Notes

  • Strip unnecessary metadata from Base64 strings before upload.
  • Use beforeChange to transform and sanitize image inputs before persistence.
  • Validate permissions and tokens when serving private resources (e.g., images on S3).
  • Apply size limits. The REST API already caps each form field at 50 MB and rejects requests that exceed it; add stricter limits in beforeChange if you need them.