Skip to content

Commit 57cba4a

Browse files
committed
POC
1 parent b488865 commit 57cba4a

4 files changed

Lines changed: 128 additions & 3 deletions

File tree

addons/addons.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,5 +188,6 @@
188188
"remove-curved-stage-border",
189189
"columns",
190190
"editor-compact",
191-
"editor-theme3"
191+
"editor-theme3",
192+
"webp"
192193
]

addons/better-img-uploads/userscript.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export default async function ({ addon, console, msg }) {
2525
});
2626
button.append(img);
2727
const input = Object.assign(document.createElement("input"), {
28-
accept: ".svg, .png, .bmp, .jpg, .jpeg, .sprite2, .sprite3",
28+
accept: ".svg, .png, .bmp, .jpg, .jpeg, .webp, .sprite2, .sprite3",
2929
className: `${addon.tab.scratchClass(
3030
"action-menu_file-input" /* TODO: when adding dynamicDisable, ensure compat with drag-drop */
3131
)} sa-better-img-uploads-input`,
@@ -110,7 +110,7 @@ export default async function ({ addon, console, msg }) {
110110
let processed = new Array();
111111

112112
for (let file of files) {
113-
if (!/\.(png|jpe?g|bmp)$/i.test(file.name)) {
113+
if (!/\.(png|jpe?g|bmp|webp)$/i.test(file.name)) {
114114
// The file is not processable, so we should ignore it, and let scratch deal with it..
115115
processed.push(file);
116116
continue;

addons/webp/addon.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"name": "WebP Support",
3+
"description": "Adds support for WebP images to be uploaded. They will be converted to another format on upload.",
4+
"tags": ["editor", "costumeEditor", "recommended"],
5+
"versionAdded": "1.43.0",
6+
"credits": [{ "name": "Jazza", "link": "https://github.com/Jazza-231" }],
7+
"enabledByDefault": true,
8+
"settings": [
9+
{
10+
"name": "Image format",
11+
"id": "format",
12+
"type": "select",
13+
"potentialValues": [
14+
{ "id": "jpeg", "name": "JPEG" },
15+
{ "id": "png", "name": "PNG" }
16+
],
17+
"default": "jpeg"
18+
},
19+
{ "name": "Quality", "id": "quality", "type": "integer", "min": 0, "max": 100, "default": 70 }
20+
],
21+
"userscripts": [{ "matches": ["projects"], "url": "userscript.js" }]
22+
}

addons/webp/userscript.js

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
export default async function ({ addon }) {
2+
while (true) {
3+
const spriteSelector = '[class*="sprite-selector_sprite-selector_"] [class*="action-menu_more-buttons_"] input';
4+
const stageSelector = '[class*="stage-selector_stage-selector_"] [class*="action-menu_more-buttons_"] input';
5+
const costumeSelector = '[data-tabs] > :nth-child(3) [class*="action-menu_more-buttons_"] input';
6+
let menuInput = await addon.tab.waitForElement(`${spriteSelector}, ${stageSelector}, ${costumeSelector}`, {
7+
markAsSeen: true,
8+
reduxCondition: (state) => !state.scratchGui.mode.isPlayerOnly,
9+
reduxEvents: [
10+
"scratch-gui/mode/SET_PLAYER",
11+
"fontsLoaded/SET_FONTS_LOADED",
12+
"scratch-gui/locales/SELECT_LOCALE",
13+
"scratch-gui/navigation/ACTIVATE_TAB",
14+
],
15+
});
16+
17+
const acceptsStr = menuInput.accept.trim();
18+
// Set to dedupe
19+
const accepts = new Set(acceptsStr.split(",").map((s) => s.trim()));
20+
accepts.add(".webp");
21+
22+
menuInput.accept = Array.from(accepts).join(", ");
23+
24+
// Let HD uploads handle it
25+
if (menuInput.className.includes("sa-better-img-uploads-input")) continue;
26+
27+
menuInput.addEventListener("change", async (e) => {
28+
if (e.detail === "converted") return;
29+
30+
const input = e.target;
31+
const files = input.files;
32+
33+
if (files.length === 0) return;
34+
35+
for (const file of files) {
36+
console.log(file);
37+
38+
const isWebP = file.name.endsWith(".webp") || file.type === "image/webp";
39+
if (!isWebP) return;
40+
41+
// Hide it from scratch hehe
42+
e.stopImmediatePropagation();
43+
e.preventDefault();
44+
45+
try {
46+
const buffer = await file.arrayBuffer();
47+
const blob = new Blob([buffer], { type: "image/webp" });
48+
const imageURL = URL.createObjectURL(blob);
49+
50+
const image = new Image();
51+
52+
await new Promise((resolve, reject) => {
53+
image.onload = () => resolve();
54+
image.onerror = () => reject("Failed to load WebP image");
55+
image.src = imageURL;
56+
});
57+
58+
const canvas = document.createElement("canvas");
59+
canvas.width = image.width;
60+
canvas.height = image.height;
61+
const ctx = canvas.getContext("2d");
62+
63+
ctx.drawImage(image, 0, 0);
64+
URL.revokeObjectURL(imageURL);
65+
66+
// Convert to chosen format
67+
let format = addon.settings.get("format");
68+
69+
console.log(addon.settings.get("format"), addon.settings.get("quality"));
70+
71+
const newBlob = await new Promise((resolve, reject) => {
72+
canvas.toBlob(
73+
(b) => (b ? resolve(b) : reject(new Error("Encode failed"))),
74+
`image/${format}`,
75+
addon.settings.get("quality") / 100
76+
);
77+
});
78+
79+
// Create a new file
80+
const newName = file.name.replace(".webp", `.${format}`);
81+
const newFile = new File([newBlob], newName, { type: `image/${format}` });
82+
83+
console.log(newFile);
84+
85+
const dt = new DataTransfer();
86+
dt.items.add(newFile);
87+
input.files = dt.files;
88+
89+
// Send it through back to scratch :D
90+
const evt = new CustomEvent("change", { bubbles: true, detail: "converted" });
91+
input.dispatchEvent(evt);
92+
} catch (error) {
93+
console.error(error);
94+
95+
// Default, just send it through for scratch to reject if smth messed up
96+
const evt = new Event("change", { bubbles: true });
97+
input.dispatchEvent(evt);
98+
}
99+
}
100+
});
101+
}
102+
}

0 commit comments

Comments
 (0)