Skip to content

Commit

Permalink
Apply image preferences to everything & bug fixes
Browse files Browse the repository at this point in the history
- Apply image preferences to everything: by clicking a button, the user can copy the filter, quality, format or width/height preferences of an image and apply it on every other one
- Bug fixes: fixed a Promise that caused a missed re-rendering of the canvas when exporting
- Added license for filter context polyfill
  • Loading branch information
dinoosauro committed Jun 10, 2024
1 parent 7c8baa2 commit 6c47a86
Show file tree
Hide file tree
Showing 9 changed files with 178 additions and 27 deletions.
54 changes: 54 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,57 @@

Convert images frmo various formats to JPG/PNG/WebP, by resizing them and adding
filters.

## How it works

### Choose a file

Open image-converter from the link above. You'll be prompted to choose some
files. You can choose everything from a folder, or you can manually pick the
files you want. You can also drop the images from the system's file picker.

![The UI of the "Select file" part of the screen](./readme-assets/StartUI.jpg)

### Change image

After that, a new page will be shown. At the top, all of the images you've
selected will be shown. Click on each photo to change the values of that one.

![The array of images](./readme-assets/Images.jpg)

### Edit the width and height

Now you can choose to edit the width/height of the image: you can change them
with a percentage, or you can specify a fixed width or height (the other value
will be adapted by keeping the same aspect ratio)

![Edit image width/height with a percentage](./readme-assets/PercentageContent.jpg)

### Apply filters

Then, you can apply filters to the image. Those filters are the same you can
apply to CSS items.

![Apply filters to image](./readme-assets/FilterContent.jpg)

### Image preview

Finally, you can see a preview of your image, rendered with the same
width/height specified in the "Resize content" tab.

You can export only the current image, or all the images you've selected. Keep
in mind that, by default, image-converter will try using the File System API for
a native-like experience, but you can still use the normal file download by
disabling this feature in the settings.

![The canvas preview UI](./readme-assets/CanvasPreview.jpeg)

## Settings

In the settings, you can:

- Edit the application theme (by changing also the colors used)
- Disable the File System API, that permits to write the images directly in a
specific folder selected by the user (Currently available only on
Chromium-based browsers)
- See the open source licenses
Binary file added readme-assets/CanvasPreview.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added readme-assets/FilterContent.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added readme-assets/Images.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added readme-assets/PercentageContent.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added readme-assets/StartUI.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 4 additions & 1 deletion src/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
{ name: "Svelte", extra: "2016-24 these people" },
{ name: "heic2any", extra: "2020 Alex Corvi" },
{ name: "UTIF.js", extra: "2017 Photopea" },
{ name: "context-filter-polyfill", extra: "2019 David Enke" },
];
$: selectedLicense = "JSZip";
function applyNewTheme(prop: string, val: string) {
Expand Down Expand Up @@ -174,7 +175,9 @@
id={fileSystemAPIId}
on:change={fileSystemAPIChange}
/>
<label for={fileSystemAPIId}>Use File System API if available</label>
<label for={fileSystemAPIId}
>Avoid using the File System API (if available)</label
>
</div>
</div>
<br /><br />
Expand Down
1 change: 1 addition & 0 deletions src/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ button:active {
height: 10vh;
border-radius: 8px;
flex-shrink: 0;
object-fit: cover;
}

.multiPage {
Expand Down
145 changes: 119 additions & 26 deletions src/lib/ImageEditing/CanvasRender.svelte
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
<script lang="ts">
import { hasContext, onMount } from "svelte";
import { afterUpdate, hasContext, onMount } from "svelte";
import {
conversionProgress,
convertFiles,
currentImageEditing,
forceCanvasUpdate,
measureMap,
type FileConversion,
} from "../../Scripts/Storage";
import { ExportFile, getZip, restoreZip } from "../../Scripts/ExportFile";
import FileSystemHandle from "../../Scripts/FileSystemHandle";
Expand All @@ -24,8 +25,8 @@
img.src = URL.createObjectURL(
$convertFiles[$currentImageEditing].blob,
);
img.onload = () => {
reRender();
img.onload = async () => {
await reRender();
resolve();
};
img.onerror = () => reject();
Expand Down Expand Up @@ -65,26 +66,32 @@
}
}
function reRender() {
if (canvas === undefined) return;
const ctx = canvas.getContext("2d");
if (ctx) {
const spinner = createSpinner();
setTimeout(() => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
const proportions = getProportions();
canvas.width = proportions[0];
canvas.height = proportions[1];
let outputStr = "";
for (let key in $convertFiles[$currentImageEditing].filters ??
{}) {
// @ts-ignore
outputStr += `${key}(${$convertFiles[$currentImageEditing].filters[key]}${measureMap.get(key)}) `;
}
ctx.filter = outputStr;
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
spinner.remove();
}, 40);
}
return new Promise<void>((resolve, reject) => {
if (canvas === undefined) {
console.warn("Canvas undefined!");
reject();
}
const ctx = canvas.getContext("2d");
if (ctx) {
const spinner = createSpinner();
setTimeout(() => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
const proportions = getProportions();
canvas.width = proportions[0];
canvas.height = proportions[1];
let outputStr = "";
for (let key in $convertFiles[$currentImageEditing]
.filters ?? {}) {
// @ts-ignore
outputStr += `${key}(${$convertFiles[$currentImageEditing].filters[key]}${measureMap.get(key)}) `;
}
ctx.filter = outputStr;
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
spinner.remove();
resolve();
}, 40);
} else reject();
});
}
onMount(() => updateView());
forceCanvasUpdate.subscribe(() => reRender());
Expand Down Expand Up @@ -123,6 +130,50 @@
);
});
}
interface BatchConvertObj {
id: string;
update: boolean;
showText: string;
updateVal: (keyof FileConversion)[];
}
let batchConvertItems: BatchConvertObj[] = [];
const batchConvertText = [
"Image output format",
"Width/height settings",
"Quality settings",
"Filter settings",
];
const availableOptions: (keyof FileConversion)[][] = [
["mimeType"],
["scaleType", "scale"],
["quality"],
["filters"],
];
for (let i = 0; i < 4; i++)
batchConvertItems[i] = {
id: `Checkbox-${Math.random()}`,
update: false,
showText: batchConvertText[i],
updateVal: availableOptions[i],
};
function updateBatchCheckbox(e: Event, ref: number) {
batchConvertItems[ref].update = (e.target as HTMLInputElement).checked;
}
function applyBatchCheckbox() {
for (let i = 0; i < batchConvertItems.length; i++) {
if (batchConvertItems[i].update)
for (let x = 0; x < $convertFiles.length; x++) {
for (let option of batchConvertItems[i].updateVal)
$convertFiles[x][option as "blob"] = $convertFiles[
$currentImageEditing
][option] as Blob;
}
}
batchDialog.style.opacity = "0";
setTimeout(() => (showBatchDialog = false), 210);
}
let batchDialog: HTMLDivElement;
$: showBatchDialog = false;
</script>

<div class="card">
Expand All @@ -135,7 +186,7 @@
const name = formatFileName($currentImageEditing);
let handle;
try {
if (localStorage.getItem("ImageConverter-FSApi") !== "a")
if (localStorage.getItem("ImageConverter-FSApi") === "a")
throw new Error(
"The user has disabled the usage of File System API",
);
Expand Down Expand Up @@ -170,7 +221,7 @@
on:click={async () => {
let handle;
try {
if (localStorage.getItem("ImageConverter-FSApi") !== "a")
if (localStorage.getItem("ImageConverter-FSApi") === "a")
throw new Error(
"The user has disabled the usage of File System API",
);
Expand Down Expand Up @@ -200,7 +251,21 @@
}}>Export all images</button
>
</div>
<br />
<button
style="background-color: var(--second); margin-top: 5px;"
on:click={() => {
showBatchDialog = true;
// TODO: Move this on afterMount, this solution is horrible
const interval = setInterval(() => {
if (!batchDialog) return;
batchDialog.style.opacity = "1";
for (let i = 0; i < batchConvertItems.length; i++)
batchConvertItems[i].update = false;
clearInterval(interval);
}, 15);
}}>Apply current preferences to every image</button
>
<br /><br />
<div class="checkContainer">
<input type="checkbox" id={zipId} bind:checked={exportAsZip} /><label
for={zipId}>Save as a zip file</label
Expand All @@ -212,6 +277,34 @@
</div>
</div>

{#if showBatchDialog}
<div class="dialogContainer" bind:this={batchDialog}>
<div class="fullDialog">
<TitleIcon asset="image">Batch conversion settings:</TitleIcon>
<p>
From each slider below, choose which properties of this image
should be applied to every image currently uploaded. Note that,
if you upload more images, you'll need to apply these properties
again.
</p>
<br />
{#each batchConvertItems as text, i}
<div class="second card" style="margin-bottom: 10px;">
<div class="checkContainer">
<input
type="checkbox"
id={text.id}
on:change={(e) => updateBatchCheckbox(e, i)}
/><label for={text.id}>{text.showText}</label>
</div>
</div>
{/each}
<br /><br />
<button on:click={() => applyBatchCheckbox()}>Apply</button>
</div>
</div>
{/if}

<style>
canvas {
object-fit: contain;
Expand Down

0 comments on commit 6c47a86

Please sign in to comment.