Agamnentzar / ag-psd

Javascript library for reading and writing PSD files
Other
489 stars 66 forks source link

Generating thumbnail significantly increases file size #156

Closed imanbell closed 11 months ago

imanbell commented 11 months ago

Hi!

I'm trying to generate a thumbnail when writing my PSD file. For this it seems that I have to use the imageData field in the PSD parameters to set my thumbnail image. The image in imageData must have the same dimensions as the PSD document (i.e. potentially quite big). When I tried it, I get my thumbnail, but the PSD file size becomes much bigger compared to a PSD without a custom thumbnail (for example, it goes from 7.8 MB to 20.5 MB for a 5000x5000 PSD with 2 layers). Is there a way to only generate/set a small thumbnail without using this imageData field, in order to avoid the file size increase? Am I misunderstanding its purpose?

Thanks!

Agamnentzar commented 11 months ago

The automatic thumbnail is generated from the composite image of the drawing, it's always small and compressed with JPEG compression so it's not an issue, what is taking the space is the composite image that you're setting. Instead of setting composite image and relying on automatic thumbnail generation you can just set the thumbnail yourself in psd.thumbnail and NOT set the composite image. Here's how the thumbnail is generated internally:

function createThumbnail(psd: Psd) {
    const canvas = createCanvas(10, 10);
    let scale = 1;

    if (psd.width > psd.height) {
        canvas.width = 160;
        canvas.height = Math.floor(psd.height * (canvas.width / psd.width));
        scale = canvas.width / psd.width;
    } else {
        canvas.height = 160;
        canvas.width = Math.floor(psd.width * (canvas.height / psd.height));
        scale = canvas.height / psd.height;
    }

    const context = canvas.getContext('2d')!;
    context.scale(scale, scale);

    if (psd.imageData) {
        const temp = createCanvas(psd.imageData.width, psd.imageData.height);
        temp.getContext('2d')!.putImageData(psd.imageData, 0, 0);
        context.drawImage(temp, 0, 0);
    } else if (psd.canvas) {
        context.drawImage(psd.canvas, 0, 0);
    }

    return canvas;
}
imanbell commented 11 months ago

Hi, thank you for your quick reply!

Unfortunately I didn't manage to make it work. I've tried setting the psd.thumbnail property as well as psd.imageResources.thumbnail, tried the raw versions as well, while setting generateThumbnail to false and of course not setting the composite image. When I follow the code execution it looks like it should work, but in the end the thumbnail is always black. Do you maybe have a small sample code of this working to generate a basic PSD with a thumbnail? Maybe I'm doing something wrong...

Note: I'm trying to make this work in nodeJS, but I tried it on browser too and it didn't work for me

Agamnentzar commented 11 months ago

My mistake it was psd.imageResources.thumbnail. Can you show me your code?

imanbell commented 11 months ago

psdWriter.zip

Here is a simple nodeJS example (in the folder run 'npm i' and then 'node index.js'). It's supposed to generate a PSD with 3 layers, and set a pink thumbnail. The PSD is generated correctly, but the thumbnail is black. Thanks for looking into this!

Agamnentzar commented 11 months ago

You're generating pink PNG image when writing it to a buffer and then using these bytes as pixels, that is not going to work.

Try something like this

    const canvas = createCanvas(160, 160);
    const ctx = canvas.getContext("2d")!;
    ctx.fillStyle = 'pink';
    ctx.fillRect(0, 0, 160, 160);
    return canvas;

If you want to use thumbnailRaw is just raw pixels so no need to use any libraries, you just use Uint8Array of pixel data, you can just fill it yourself with bytes corresponding to pink pixels.

If you set both thumbnailRaw and thumbnail the thumbnailRaw will be used and thumbnail will be ignored.

imanbell commented 11 months ago

No, my getImageData function should return the width, height and the pixel data (if I'm correct on how the pngjs library works).

But in any case, I just tried the method you shared about creating the canvas directly and setting it on the thumbnail field (and putting nothing in thumbnailRaw) and the thumbnail is still black... has it worked for you?

Note: the colors I'm using to create the layers and the thumbnail are just examples, to create a simple scenario that reproduces my issue.

Agamnentzar commented 11 months ago

How are you checking the thumbnail ?

imanbell commented 11 months ago

I upload the file to my Google Drive, which supports PSD thumbnail previewing.

Agamnentzar commented 11 months ago

I wrote this test:

        const canvas = createCanvas(300, 200);
        const context = canvas.getContext('2d')!;
        context.fillStyle = 'pink';
        context.fillRect(0, 0, canvas.width, canvas.height);

        const canvas2 = createCanvas(300, 200);
        const context2 = canvas2.getContext('2d')!;
        context2.fillStyle = 'orange';
        context2.fillRect(0, 0, canvas.width, canvas.height);

        const psd: Psd = {
            width: 300,
            height: 200,
            canvas: canvas,
            children: [
                {
                    name: 'bg',
                    canvas: canvas,
                },
            ],
            imageResources: {
                thumbnail: canvas2,
            },
        };

        const buffer = writePsdBuffer(psd, { generateThumbnail: false });
        fs.writeFileSync(path.join(resultsFilesPath, `thumb_test.psd`), buffer);

and got pink thumbnail in google drive, so it seems google drive ignores the thumbnail and just displays composite image, so you're probably stuck with having to generate big file if you want it to show up properly in google drive.

Agamnentzar commented 11 months ago

The layer and composite images are compressed using RLE encoding so if you're using images with a lot of noise (like photos) then it will be large.

imanbell commented 11 months ago

Oh okay I think you're right. I actually tried the files on MacOS Finder, and the small thumbnail works for the file icon. However for the file preview, indeed it seems that the composite image is necessary. And as you said Google Drive probably needs the composite image too. It looks like I'll probably need to generate the composite image for my use case. Thanks a lot for your help!