From 2fb5d4ae0a4987456705a73776808a15a81627e6 Mon Sep 17 00:00:00 2001 From: decodatain Date: Sat, 26 Apr 2025 21:18:55 -0600 Subject: [PATCH] Enhance image processing settings in AnkiConnect - Added height, width, and quality fields to AnkiConnect settings. - Updated imageToWebp and getCroppedImg functions to accept settings for resizing and quality. - Integrated settings into Cropper and QuickActions components for improved image handling. Reason Note: The recommended settings of 200 and 0.5 make average file sizes of around 5kb. This is about the same filesize as the audio for a given word. With maximum quality and no file size limit, an image size of 1200x1800 pixels is around 1mb. This means 1,000 words mined with full sized pictures takes up a 1gb of space on anki. With the recommended settings, it would take 200,000 words mined to take up a GB of space on anki. These quality settings can be disabled by settings the max quality to 1 and max width and height to 0. The user is in full control of these changes. --- src/lib/anki-connect/cropper.ts | 9 ++--- src/lib/anki-connect/index.ts | 28 +++++++++++++-- src/lib/components/Reader/Cropper.svelte | 2 +- src/lib/components/Reader/QuickActions.svelte | 2 +- src/lib/components/Reader/TextBoxes.svelte | 2 +- .../Settings/AnkiConnectSettings.svelte | 36 +++++++++++++++++++ src/lib/settings/settings.ts | 6 ++++ 7 files changed, 76 insertions(+), 9 deletions(-) diff --git a/src/lib/anki-connect/cropper.ts b/src/lib/anki-connect/cropper.ts index 52d060e..b66f7b9 100644 --- a/src/lib/anki-connect/cropper.ts +++ b/src/lib/anki-connect/cropper.ts @@ -1,6 +1,6 @@ import { showSnackbar } from '$lib/util'; import { writable } from 'svelte/store'; -import { blobToBase64 } from '.'; +import { blobToBase64, imageResize } from '.'; type CropperModal = { open: boolean; @@ -34,7 +34,7 @@ function getRadianAngle(degreeValue: number) { export type Pixels = { width: number; height: number; x: number; y: number } -export async function getCroppedImg(imageSrc: string, pixelCrop: Pixels, rotation = 0) { +export async function getCroppedImg(imageSrc: string, pixelCrop: Pixels, settings: any, rotation = 0 ) { const image = await createImage(imageSrc); const canvas = new OffscreenCanvas(image.width, image.height); const ctx = canvas.getContext('2d'); @@ -71,8 +71,9 @@ export async function getCroppedImg(imageSrc: string, pixelCrop: Pixels, rotatio Math.round(0 - safeArea / 2 + image.width * 0.5 - pixelCrop.x), Math.round(0 - safeArea / 2 + image.height * 0.5 - pixelCrop.y) ); - - const blob = await canvas.convertToBlob({ type: 'image/webp' }); + + await imageResize(canvas, ctx, settings.ankiConnectSettings.widthField, settings.ankiConnectSettings.heightField); + const blob = await canvas.convertToBlob({ type: 'image/webp', quality: settings.ankiConnectSettings.qualityField }); return await blobToBase64(blob) } \ No newline at end of file diff --git a/src/lib/anki-connect/index.ts b/src/lib/anki-connect/index.ts index bd175a0..9b6a8f9 100644 --- a/src/lib/anki-connect/index.ts +++ b/src/lib/anki-connect/index.ts @@ -50,20 +50,44 @@ export async function blobToBase64(blob: Blob) { }); } -export async function imageToWebp(source: File) { +export async function imageToWebp(source: File, settings: any) { const image = await createImageBitmap(source); const canvas = new OffscreenCanvas(image.width, image.height); const context = canvas.getContext("2d"); if (context) { context.drawImage(image, 0, 0); - const blob = await canvas.convertToBlob({ type: 'image/webp' }); + await imageResize(canvas, context, settings.ankiConnectSettings.widthField, settings.ankiConnectSettings.heightField); + const blob = await canvas.convertToBlob({ type: 'image/webp', quality: settings.ankiConnectSettings.qualityField }); image.close(); return await blobToBase64(blob); } } +export async function imageResize(canvas: OffscreenCanvas, ctx: OffscreenCanvasRenderingContext2D, maxWidth: number, maxHeight: number): Promise { + return new Promise((resolve, reject) => { + const widthRatio = maxWidth <= 0 ? 1 : maxWidth / canvas.width; + const heightRatio = maxHeight <= 0 ? 1 : maxHeight / canvas.height; + const ratio = Math.min(1, Math.min(widthRatio, heightRatio)); + + if (ratio < 1) { + const newWidth = canvas.width * ratio; + const newHeight = canvas.height * ratio; + createImageBitmap(canvas, { resizeWidth: newWidth, resizeHeight: newHeight, resizeQuality: 'high' }) + .then((sprite) => { + canvas.width = newWidth; + canvas.height = newHeight; + ctx.drawImage(sprite, 0, 0); + resolve(canvas); + }) + .catch((e) => reject(e)); + } else { + resolve(canvas); + } + }); +} + export async function updateLastCard(imageData: string | null | undefined, sentence?: string) { const { overwriteImage, diff --git a/src/lib/components/Reader/Cropper.svelte b/src/lib/components/Reader/Cropper.svelte index 5b15737..1d57a09 100644 --- a/src/lib/components/Reader/Cropper.svelte +++ b/src/lib/components/Reader/Cropper.svelte @@ -37,7 +37,7 @@ async function onCrop() { if ($cropperStore?.image && pixels) { loading = true; - const imageData = await getCroppedImg($cropperStore.image, pixels); + const imageData = await getCroppedImg($cropperStore.image, pixels, $settings); updateLastCard(imageData, $cropperStore.sentence); close(); } diff --git a/src/lib/components/Reader/QuickActions.svelte b/src/lib/components/Reader/QuickActions.svelte index 072e0e6..94e34da 100644 --- a/src/lib/components/Reader/QuickActions.svelte +++ b/src/lib/components/Reader/QuickActions.svelte @@ -40,7 +40,7 @@ showCropper(URL.createObjectURL(src)); } else { promptConfirmation('Add image to last created anki card?', async () => { - const imageData = await imageToWebp(src); + const imageData = await imageToWebp(src, $settings); updateLastCard(imageData); }); } diff --git a/src/lib/components/Reader/TextBoxes.svelte b/src/lib/components/Reader/TextBoxes.svelte index 351ae56..b45553f 100644 --- a/src/lib/components/Reader/TextBoxes.svelte +++ b/src/lib/components/Reader/TextBoxes.svelte @@ -54,7 +54,7 @@ showCropper(URL.createObjectURL(src), sentence); } else { promptConfirmation('Add image to last created anki card?', async () => { - const imageData = await imageToWebp(src); + const imageData = await imageToWebp(src, $settings); updateLastCard(imageData, sentence); }); } diff --git a/src/lib/components/Settings/AnkiConnectSettings.svelte b/src/lib/components/Settings/AnkiConnectSettings.svelte index 0522bc2..a0d7fe6 100644 --- a/src/lib/components/Settings/AnkiConnectSettings.svelte +++ b/src/lib/components/Settings/AnkiConnectSettings.svelte @@ -13,6 +13,10 @@ let pictureField = $settings.ankiConnectSettings.pictureField; let sentenceField = $settings.ankiConnectSettings.sentenceField; + let heightField = $settings.ankiConnectSettings.heightField; + let widthField = $settings.ankiConnectSettings.widthField; + let qualityField = $settings.ankiConnectSettings.qualityField; + let triggerMethod = $settings.ankiConnectSettings.triggerMethod; const triggerOptions = [ @@ -90,5 +94,37 @@ /> +
+

Quality Settings

+ Allows you to customize the file size stored on your devices +
+ + {updateAnkiSetting('heightField', heightField); if (heightField < 0) heightField = 0;}} + min={0} + /> +
+
+ + {updateAnkiSetting('widthField', widthField); if (widthField < 0) widthField = 0;}} + min={0} + /> +
+
+ + updateAnkiSetting('qualityField', qualityField)} + /> +
diff --git a/src/lib/settings/settings.ts b/src/lib/settings/settings.ts index 20924af..770870e 100644 --- a/src/lib/settings/settings.ts +++ b/src/lib/settings/settings.ts @@ -28,6 +28,9 @@ export type AnkiConnectSettings = { enabled: boolean; pictureField: string; sentenceField: string; + heightField: number; + widthField: number; + qualityField: number; cropImage: boolean; overwriteImage: boolean; grabSentence: boolean; @@ -98,6 +101,9 @@ const defaultSettings: Settings = { overwriteImage: true, pictureField: 'Picture', sentenceField: 'Sentence', + heightField: 200, + widthField: 200, + qualityField: 0.5, triggerMethod: 'both' } };