Merge pull request #64 from Decodatain/image-processing

Enhance image processing settings in AnkiConnect
This commit is contained in:
Shaun Tenner
2025-06-25 20:57:42 +02:00
committed by GitHub
7 changed files with 81 additions and 10 deletions

View File

@@ -1,6 +1,7 @@
import { showSnackbar } from '$lib/util'; import { showSnackbar } from '$lib/util';
import { writable } from 'svelte/store'; import { writable } from 'svelte/store';
import { blobToBase64 } from '.'; import { blobToBase64, imageResize } from '.';
import { Settings } from '$lib/settings';
type CropperModal = { type CropperModal = {
open: boolean; open: boolean;
@@ -34,7 +35,7 @@ function getRadianAngle(degreeValue: number) {
export type Pixels = { width: number; height: number; x: number; y: 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: Settings, rotation = 0 ) {
const image = await createImage(imageSrc); const image = await createImage(imageSrc);
const canvas = new OffscreenCanvas(image.width, image.height); const canvas = new OffscreenCanvas(image.width, image.height);
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
@@ -71,8 +72,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.width * 0.5 - pixelCrop.x),
Math.round(0 - safeArea / 2 + image.height * 0.5 - pixelCrop.y) 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) return await blobToBase64(blob)
} }

View File

@@ -1,4 +1,4 @@
import { settings } from "$lib/settings"; import { Settings, settings } from "$lib/settings";
import { showSnackbar } from "$lib/util" import { showSnackbar } from "$lib/util"
import { get } from "svelte/store"; import { get } from "svelte/store";
@@ -50,20 +50,44 @@ export async function blobToBase64(blob: Blob) {
}); });
} }
export async function imageToWebp(source: File) { export async function imageToWebp(source: File, settings: Settings) {
const image = await createImageBitmap(source); const image = await createImageBitmap(source);
const canvas = new OffscreenCanvas(image.width, image.height); const canvas = new OffscreenCanvas(image.width, image.height);
const context = canvas.getContext("2d"); const context = canvas.getContext("2d");
if (context) { if (context) {
context.drawImage(image, 0, 0); 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(); image.close();
return await blobToBase64(blob); return await blobToBase64(blob);
} }
} }
export async function imageResize(canvas: OffscreenCanvas, ctx: OffscreenCanvasRenderingContext2D, maxWidth: number, maxHeight: number): Promise<OffscreenCanvas> {
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) { export async function updateLastCard(imageData: string | null | undefined, sentence?: string) {
const { const {
overwriteImage, overwriteImage,

View File

@@ -37,7 +37,7 @@
async function onCrop() { async function onCrop() {
if ($cropperStore?.image && pixels) { if ($cropperStore?.image && pixels) {
loading = true; loading = true;
const imageData = await getCroppedImg($cropperStore.image, pixels); const imageData = await getCroppedImg($cropperStore.image, pixels, $settings);
updateLastCard(imageData, $cropperStore.sentence); updateLastCard(imageData, $cropperStore.sentence);
close(); close();
} }

View File

@@ -40,7 +40,7 @@
showCropper(URL.createObjectURL(src)); showCropper(URL.createObjectURL(src));
} else { } else {
promptConfirmation('Add image to last created anki card?', async () => { promptConfirmation('Add image to last created anki card?', async () => {
const imageData = await imageToWebp(src); const imageData = await imageToWebp(src, $settings);
updateLastCard(imageData); updateLastCard(imageData);
}); });
} }

View File

@@ -54,7 +54,7 @@
showCropper(URL.createObjectURL(src), sentence); showCropper(URL.createObjectURL(src), sentence);
} else { } else {
promptConfirmation('Add image to last created anki card?', async () => { promptConfirmation('Add image to last created anki card?', async () => {
const imageData = await imageToWebp(src); const imageData = await imageToWebp(src, $settings);
updateLastCard(imageData, sentence); updateLastCard(imageData, sentence);
}); });
} }

View File

@@ -13,6 +13,10 @@
let pictureField = $settings.ankiConnectSettings.pictureField; let pictureField = $settings.ankiConnectSettings.pictureField;
let sentenceField = $settings.ankiConnectSettings.sentenceField; 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; let triggerMethod = $settings.ankiConnectSettings.triggerMethod;
const triggerOptions = [ const triggerOptions = [
@@ -90,5 +94,40 @@
/> />
</Label> </Label>
</div> </div>
<hr>
<h4>Quality Settings</h4>
<Helper>Allows you to customize the file size stored on your devices</Helper>
<div>
<Label>Max Height (0 = Ignore; 200 Recommended):</Label>
<Input
{disabled}
type="number"
bind:value={heightField}
on:change={() => {updateAnkiSetting('heightField', heightField); if (heightField < 0) heightField = 0;}}
min={0}
/>
</div>
<div>
<Label>Max Width (0 = Ignore; 200 Recommended):</Label>
<Input
{disabled}
type="number"
bind:value={widthField}
on:change={() => {updateAnkiSetting('widthField', widthField); if (widthField < 0) widthField = 0;}}
min={0}
/>
</div>
<div>
<Label>Quality (Between 0 and 1; 0.5 Recommended):</Label>
<Input
{disabled}
type="number"
bind:value={qualityField}
on:change={() => updateAnkiSetting('qualityField', qualityField)}
min={0}
max={1}
step="0.1"
/>
</div>
</div> </div>
</AccordionItem> </AccordionItem>

View File

@@ -28,6 +28,9 @@ export type AnkiConnectSettings = {
enabled: boolean; enabled: boolean;
pictureField: string; pictureField: string;
sentenceField: string; sentenceField: string;
heightField: number;
widthField: number;
qualityField: number;
cropImage: boolean; cropImage: boolean;
overwriteImage: boolean; overwriteImage: boolean;
grabSentence: boolean; grabSentence: boolean;
@@ -98,6 +101,9 @@ const defaultSettings: Settings = {
overwriteImage: true, overwriteImage: true,
pictureField: 'Picture', pictureField: 'Picture',
sentenceField: 'Sentence', sentenceField: 'Sentence',
heightField: 0,
widthField: 0,
qualityField: 1,
triggerMethod: 'both' triggerMethod: 'both'
} }
}; };