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.
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
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 '.';
|
||||||
|
|
||||||
type CropperModal = {
|
type CropperModal = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -34,7 +34,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: any, 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 +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.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)
|
||||||
}
|
}
|
||||||
@@ -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 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,
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,37 @@
|
|||||||
/>
|
/>
|
||||||
</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)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
|
|||||||
@@ -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: 200,
|
||||||
|
widthField: 200,
|
||||||
|
qualityField: 0.5,
|
||||||
triggerMethod: 'both'
|
triggerMethod: 'both'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user