Skip to content

Commit

Permalink
Use 'sharp' to resize thumbnails
Browse files Browse the repository at this point in the history
This works around an Electron crash that occurs when creating thumbnails
for very large images when using Canvas for resizing.

* Add 'sharp' library
* Bump electron minor version to deal with lovell/sharp#3384

fixup
  • Loading branch information
apage43 committed Apr 14, 2023
1 parent 77e825a commit 376883d
Show file tree
Hide file tree
Showing 5 changed files with 247 additions and 39 deletions.
3 changes: 2 additions & 1 deletion package.json
Expand Up @@ -93,7 +93,7 @@
"@typescript-eslint/eslint-plugin": "^5.52.0",
"@typescript-eslint/parser": "^5.52.0",
"css-loader": "^6.7.3",
"electron": "21.3.0",
"electron": "21.3.4",
"electron-builder": "23.6.0",
"eslint": "^8.34.0",
"eslint-config-prettier": "^8.6.0",
Expand Down Expand Up @@ -135,6 +135,7 @@
"react-colorful": "^5.6.1",
"react-dom": "^18.2.0",
"react-window": "^1.8.8",
"sharp": "^0.32.0",
"sourcemapped-stacktrace": "^1.1.11",
"utif": "^3.1.0",
"wasm-feature-detect": "^1.2.11"
Expand Down
44 changes: 17 additions & 27 deletions src/frontend/workers/thumbnailGenerator.worker.ts
@@ -1,44 +1,34 @@
import fse from 'fs-extra';

import * as sharp from 'sharp';
import { thumbnailFormat, thumbnailMaxSize } from 'common/config';
import { IThumbnailMessage, IThumbnailMessageResponse } from '../image/ThumbnailGeneration';

// TODO: Merge this with the generateThumbnail func from frontend/image/utils.ts, it's duplicate code
const generateThumbnailData = async (filePath: string): Promise<ArrayBuffer | null> => {
const inputBuffer = await fse.readFile(filePath);
const inputBlob = new Blob([inputBuffer]);
const img = await createImageBitmap(inputBlob);
const img = await sharp(inputBuffer);
const metadata = await img.metadata();

// Scale the image so that either width or height becomes `thumbnailMaxSize`
let width = img.width;
let height = img.height;
if (img.width >= img.height) {
if (typeof metadata.width == 'undefined' || typeof metadata.height == 'undefined') {
throw 'could not read image dimensions';
}
const iwidth = metadata.width;
const iheight = metadata.height;
let width = iwidth;
let height = iheight;
if (iwidth >= iheight) {
width = thumbnailMaxSize;
height = (thumbnailMaxSize * img.height) / img.width;
height = Math.trunc((thumbnailMaxSize * iheight) / iwidth);
} else {
height = thumbnailMaxSize;
width = (thumbnailMaxSize * img.width) / img.height;
}

const canvas = new OffscreenCanvas(width, height);

const ctx2D = canvas.getContext('2d');
if (!ctx2D) {
console.warn('No canvas context 2D (should never happen)');
return null;
width = Math.trunc((thumbnailMaxSize * iwidth) / iheight);
}

// Todo: Take into account rotation. Can be found with https://www.npmjs.com/package/node-exiftool

// TODO: Could maybe use https://www.electronjs.org/docs/api/native-image#imageresizeoptions

ctx2D.drawImage(img, 0, 0, width, height);

const thumbBlob = await canvas.convertToBlob({ type: `image/${thumbnailFormat}`, quality: 0.75 });
// TODO: is canvas.toDataURL faster?
const reader = new FileReaderSync();
const buffer = reader.readAsArrayBuffer(thumbBlob);
return buffer;
const resized = img
.resize({ width: width, height: height, fit: sharp.fit.cover })
.toFormat(thumbnailFormat, { quality: 75 });
return await resized.toBuffer();
};

const generateAndStoreThumbnail = async (filePath: string, thumbnailFilePath: string) => {
Expand Down
3 changes: 3 additions & 0 deletions webpack.dev.js
Expand Up @@ -70,6 +70,9 @@ let rendererConfig = {
wasm: path.resolve(__dirname, 'wasm/'),
},
},
externals: {
'sharp': 'commonjs sharp'
},
module: {
rules: [
{
Expand Down
3 changes: 3 additions & 0 deletions webpack.prod.js
Expand Up @@ -68,6 +68,9 @@ let rendererConfig = {
wasm: path.resolve(__dirname, 'wasm/'),
},
},
externals: {
'sharp': 'commonjs sharp'
},
module: {
rules: [
{
Expand Down

0 comments on commit 376883d

Please sign in to comment.