Re-enabled stubs for PNG analyser and ICO maker, Implemented Data utils, file utils and PNG parser, Still testing

Update png-analyser.yml, png-chunk-analyser.yml, and 17 more files...
This commit is contained in:
2025-04-03 21:55:09 +02:00
parent 92d7b245a2
commit df93ee47b2
16 changed files with 519 additions and 184 deletions

View File

@@ -0,0 +1,7 @@
applets:
- id: "png-analyser"
resources:
scripts:
- "applet://png-analyser.mjs"
stylesheets:
- "applet://png-analyser.css"

View File

@@ -1,7 +0,0 @@
applets:
- id: "png-chunk-analyser"
resources:
scripts:
- "applet://png-chunk-analyser.mjs"
stylesheets:
- "applet://png-chunk-analyser.css"

View File

@@ -4,7 +4,8 @@ meta.title: "Icon Maker"
meta.description: "..."
file.selection.title: "File Selection"
file.selection.1: "Drop your file(s) here or click on the buttons."
enable.expert.mode: "Enable expert mode"
enable.binary.blobs: "Allow binary blobs"

View File

@@ -4,7 +4,8 @@ meta.title: "Fabricateur d'icônes"
meta.description: "..."
file.selection.title : "Sélection de fichier(s)"
file.selection.1 : "Déposez vos fichiers ici ou cliquez sur les boutons."
enable.expert.mode: "Activer le mode expert"
enable.binary.blobs: "Autoriser les blobs binaires"

View File

@@ -16,7 +16,7 @@ tools:
title_key: "meta.title"
description_key: "meta.description"
index:
priority: 100
priority: 1500
enable: true
title_key: "meta.title"
preamble_key: "meta.description"
@@ -27,4 +27,5 @@ tools:
title_key: "meta.title"
subtitle_key: "article.subtitle"
tags:
- "undefined"
- "utility"
- "graphics"

View File

@@ -1,6 +1,6 @@
tools:
- id: "png-chunk-analyser"
applet_id: "png-chunk-analyser"
- id: "png-analyser"
applet_id: "png-analyser"
metadata:
head:
title_key: "meta.title"

View File

@@ -0,0 +1,8 @@
.ico-maker-advanced {
display: none;
}
input[type="checkbox"]#ico-maker-enable-expert-mode:checked ~ .ico-maker-advanced {
display: block;
}

View File

@@ -0,0 +1,27 @@
import {initCore} from "../../js/nibblepoker-core.mjs"
import {parsePngFile} from "../../libs/png-utils.mjs";
{
initCore();
const toolId = "png-analyser";
const eFileInput = document.getElementById(`${toolId}-test-input`);
window.onload = function () {
eFileInput.addEventListener('change', function(e) {
let files = e.target.files;
console.log(files);
parsePngFile(files[0]).then(pngFile => {
console.log(pngFile);
console.log(pngFile.getImageHeaderChunk().getWidth());
console.log(pngFile.getImageHeaderChunk().getHeight());
});
});
};
}

View File

@@ -0,0 +1,52 @@
/**
* Compares two UintXArray and checks if their content is the same.
* @param array1 {Uint8Array|Uint16Array|Uint32Array}
* @param array2 {Uint8Array|Uint16Array|Uint32Array}
* @return {boolean}
*/
export function areUintArraysEqual(array1, array2) {
if ((typeof array1) !== (typeof array2)) {
return false;
}
if (array1.length !== array2.length) {
return false;
}
for (let i = 0; i < array1.length; i++) {
if (array1[i] !== array2[i]) {
return false;
}
}
return true;
}
/**
* Peeks a UInt32 from the given data at the given offset in Big-Endian.
* @param data {Uint8Array} - Data to read from.
* @param offset {number} - Offset to read from in the given `data`.
* @return {number} The peeked number.
* @throws RangeError If the given offset is too close or over the end of the data.
*/
export function peekUInt32BE(data, offset = 0) {
if(offset + 4 > data.length) {
throw new RangeError(`Offset is too far into the given data ! (${offset} & ${data.length})`);
}
return new DataView(data.buffer, offset, 4).getUint32(0, false);
}
/**
* Peeks a UInt32 from the given data at the given offset in Little-Endian.
* @param data {Uint8Array} - Data to read from.
* @param offset {number} - Offset to read from in the given `data`.
* @return {number} The peeked number.
* @throws RangeError If the given offset is too close or over the end of the data.
*/
export function peekUInt32LE(data, offset = 0) {
if(offset + 4 > data.length) {
throw new RangeError(`Offset is too far into the given data ! (${offset} & ${data.length})`);
}
return new DataView(data.buffer, offset, 4).getUint32(0, true);
}

View File

@@ -0,0 +1,28 @@
/**
* Reads a file and returns its content as a string.
* @param {File} file - The file to read.
* @returns {Promise<string>} A promise that resolves with the file content as a string.
*/
export function loadFileAsText(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsText(file);
});
}
/**
* Reads a file and returns its content as a `Uint8Array`.
* @param {File} file - The file to read.
* @returns {Promise<Uint8Array>} A promise that resolves with the file content as a byte buffer.
*/
export function loadFileAsUint8Array(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(new Uint8Array(reader.result));
reader.onerror = reject;
reader.readAsArrayBuffer(file);
});
}

View File

@@ -0,0 +1,233 @@
//import {__crc32, crc32b, _crc32b, decimalToHexString} from "./crc32.mjs";
import {areUintArraysEqual, peekUInt32BE, peekUInt32LE} from "./data-utils.mjs"
import {loadFileAsUint8Array} from "./file-utils.mjs";
/**
* Parent class extended by all PNG-related errors.
*/
export class PngError extends Error {}
export class PngInvalidFileHeaderError extends PngError {}
export class PngInvalidStructureError extends PngError {}
export class PngInvalidChunkNameError extends PngError {}
export class PngInvalidImageHeaderError extends PngError {}
export const PngFileHeader = new Uint8Array([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]);
export class PngChunk {
/** @type {string} */
type;
/** @type {Uint8Array} */
data;
/**
* @param type {string}
* @param data {Uint8Array}
* @param expectedChecksum {Uint8Array|null}
* @throws PngInvalidChunkNameError If the given chunk name isn't a valid chunk name.
*/
constructor(type, data, expectedChecksum) {
this.type = type;
this.data = data;
}
getChecksum() {
throw new Error("This function isn't implemented yet !");
}
}
class PngImageHeaderChunk extends PngChunk {
/**
* @param type {string}
* @param data {Uint8Array}
* @param expectedChecksum {Uint8Array|null}
* @throws PngInvalidChunkNameError If the given chunk name isn't a valid chunk name.
* @throws PngInvalidImageHeaderError If the given chunk's size isn't exactly 13 bytes.
*/
constructor(type, data, expectedChecksum) {
super(type, data, expectedChecksum);
if(this.data.length !== 13) {
throw new PngInvalidImageHeaderError(`Invalid IHDR chunk size, got ${this.data.length} instead of 13 !`);
}
}
getWidth() {
return peekUInt32BE(this.data, 0);
}
getHeight() {
return peekUInt32BE(this.data, 4);
}
getBitDepth() {
return this.data[8];
}
getColorType() {
return this.data[9];
}
getCompressionMethod() {
return this.data[10];
}
getFilterMethod() {
return this.data[11];
}
getInterlaceMethod() {
return this.data[12];
}
/**
* Converts a `PngChunk` to a `PngImageHeaderChunk`.
* @param pngChunk {PngChunk}
* @return {PngImageHeaderChunk}
*/
static fromPngChunk(pngChunk) {
return new PngImageHeaderChunk(pngChunk.type, pngChunk.data, null);
}
}
export class PngFile {
/** @type {File|null} */
originalFile;
/**
* Optional trailing data located after the 'IEND' chunk.
* @type {Uint8Array|null}
*/
trailingData;
/** @type {PngChunk[]} */
chunks;
/**
* @param file {File|null}
* @param fileData {Uint8Array|null}
* @throws PngInvalidFileHeaderError If the `fileData` is provided and doesn't contain a valid PNG file header.
*/
constructor(file = null, fileData = null) {
this.originalFile = file;
this.chunks = [];
// Parsing the data
if(fileData !== null) {
this.#validateFileHeader(fileData);
this.#parseChunks(fileData);
}
}
/**
* @param originalFileData {Uint8Array}
*/
#validateFileHeader = (originalFileData) => {
if(originalFileData.length < 8) {
throw new PngInvalidFileHeaderError(
`The file header's length is smaller than the required 8 ! (Got: ${originalFileData.length})`);
}
if(!areUintArraysEqual(originalFileData.slice(0, 8), PngFileHeader)) {
throw new PngInvalidFileHeaderError(
"The file header didn't have the expected data !\n" +
`Expected: ${PngFileHeader}\\n` +
`Got: ${originalFileData.slice(0, 8)}`
);
}
}
/**
* @param originalFileData {Uint8Array}
* @throws TypeError If a chunk's type couldn't be parsed.
*/
#parseChunks = (originalFileData) => {
let currentOffset = PngFileHeader.length;
while(currentOffset < originalFileData.length) {
// Checking if we haven't encountered an IEND, and we encountered a truncated file or trash data.
if(currentOffset + 12 > originalFileData.length) {
throw new PngInvalidStructureError("Unable to parse more chunks and no 'IEND' was encountered !");
}
const chunkLength = peekUInt32BE(originalFileData.slice(currentOffset, currentOffset + 4));
const chunkType = new TextDecoder().decode(originalFileData.slice(currentOffset + 4, currentOffset + 8));
// Checking if we have enough data left to read for the chunk's data.
if(currentOffset + 12 + chunkLength > originalFileData.length) {
throw new PngInvalidStructureError("Not enough data left to read the chunk !")
}
let chunkChecksum = originalFileData.slice(
currentOffset + 8 + chunkLength, currentOffset + 8 + chunkLength + 4);
this.chunks.push(
new PngChunk(
chunkType,
(chunkLength === 0) ?
new Uint8Array(0) :
originalFileData.slice(currentOffset + 8, currentOffset + 8 + chunkLength),
chunkChecksum,
//originalFileData.slice(currentOffset + 4, currentOffset + 8)
)
);
if(chunkType === "IEND") {
break;
}
currentOffset += 12 + chunkLength;
}
// Handling trailing data
if(currentOffset !== originalFileData.length) {
this.trailingData = originalFileData.slice(currentOffset, originalFileData.length);
}
}
/**
* Attempts to retrieve a chunk via its type.
* @param chunkName {string} The desired chunk's type.
* @return {PngChunk|null}
*/
getChunkByType(chunkName) {
for(let iChunk in this.chunks) {
if(this.chunks[iChunk].type === chunkName) {
return this.chunks[iChunk];
}
}
return null;
}
hasEndChunk() {
return this.getChunkByType("IEND") !== null;
}
/**
* @return {PngImageHeaderChunk|null}
*/
getImageHeaderChunk() {
let desiredChunk = this.getChunkByType("IHDR");
if(desiredChunk !== null) {
desiredChunk = PngImageHeaderChunk.fromPngChunk(desiredChunk);
}
return desiredChunk;
}
}
/**
* Reads and parses a given PNG file.
* @param {File} file - The PNG file to process.
* @returns {Promise<PngFile>} A promise that resolves with parsed PNG file.
*/
export function parsePngFile(file) {
return loadFileAsUint8Array(file).then(byteBuffer => {
return new PngFile(file, byteBuffer);
});
}

View File

@@ -1,26 +1,14 @@
<section id="{{ applet_data.id }}-introduction">
{{ render_h2(l10n("introduction.title", "commons", user_lang)) }}
{{ render_paragraph(l10n("introduction.1", applet_data.id, user_lang)) }}
{{ render_paragraph(l10n("introduction.2", applet_data.id, user_lang)) }}
</section>
{{ render_file_input(applet_data.id + "-image-input-file", true, ".png, .bmp, .ico", true, false, user_lang) }}
<section id="{{ applet_data.id }}-input">
{{ render_h2(l10n("input.title", "commons", user_lang)) }}
{{ render_paragraph(l10n("input.1", applet_data.id, user_lang)) }}
<hr class="subtle">
{{ render_file_input(applet_data.id + "-image-input-file", true, ".png, .bmp, .ico", true, false, user_lang) }}
<hr class="subtle">
<label for="{{ applet_data.id }}-enable-expert-mode" class="mr-xxs">
{{ l10n("enable.expert.mode", applet_data.id, user_lang) }}:
</label>
<input id="{{ applet_data.id }}-enable-expert-mode" class="r-m border" type="checkbox">
<!--<span class="ml-s t-italic t-muted">Toggles some uncommon features.</span>-->
<br>
<label for="{{ applet_data.id }}-enable-expert-mode" class="mr-xxs">
{{ l10n("enable.expert.mode", applet_data.id, user_lang) }}:
</label>
<input id="{{ applet_data.id }}-enable-expert-mode" class="r-m border" type="checkbox">
<div class="ico-maker-advanced">
<label for="{{ applet_data.id }}-enable-binary-blobs" class="mr-xxs">
{{ l10n("enable.binary.blobs", applet_data.id, user_lang) }}:
</label>
@@ -35,176 +23,174 @@
<span class="f-right">
{{ render_button("<i class=\"fa-duotone fa-solid fa-remove\"></i>Remove Binary Blobs", False, None, "bkgd-orange") }}
{{ render_button("<i class=\"fa-duotone fa-solid fa-remove\"></i>Remove Everything", False, None, "bkgd-red") }}
<!--{{ render_button("<i class=\"fa-duotone fa-solid fa-remove\"></i>Remove Everything", False, None, "bkgd-red") }}-->
</span>
</section>
</div>
<section id="{{ applet_data.id }}-icon-parts">
{{ render_h2(l10n("idk01.title", "commons", user_lang)) }}
{{ render_paragraph(l10n("idk01.1", applet_data.id, user_lang)) }}
<hr class="subtle">
{{ render_button("<i class=\"fa-duotone fa-solid fa-download\"></i>Download <span class=\"t-monospace\">.ico</span>", False, None, "bkgd-green") }}
{{ render_button("<i class=\"fa-duotone fa-solid fa-download\"></i>Download <span class=\"t-monospace\">.ico</span>", False, None, "bkgd-green") }}
<div class="border r-m ico-maker-advanced">
<table class="table-p-xxs ">
<tr>
<td class="bkgd-grid40 rl-m br t-size-14 t-noselect" rowspan="2">
<i class="fad fa-grip-vertical c-not-allowed px-xxs t-super-muted"></i>
</td>
<td class="w-full bkgd-gray">
<div class="px-xxs">
<p>Required ICO Headers</p>
</div>
</td>
</tr>
<tr>
<td class="bkgd-gray">
<div class="px-xxs">
<p class="t-italic t-half-muted">
<i class="fa-duotone fa-solid fa-weight-hanging mr-xs"></i>1.23 KiB
</p>
</div>
</td>
</tr>
</table>
</div>
<div id="{{ applet_data.id }}-icon-parts-list">
<div class="border r-m">
<table class="table-p-xxs ">
<table class="table-p-xxs table-v-center">
<tr>
<td class="bkgd-grid40 rl-m br t-size-14 t-noselect" rowspan="2">
<i class="fad fa-grip-vertical c-not-allowed px-xxs t-super-muted"></i>
<td class="bkgd-grid40 rl-m br t-size-14 t-noselect">
<i class="fad fa-grip-vertical cursor-pointer px-xxs"></i>
</td>
<td class="w-full bkgd-gray">
<td class="w-full bkgd-gray bb">
<div class="px-xxs">
<p>Required ICO Headers</p>
<p class="t-bold">
<i class="fa-duotone fa-solid fa-image mr-xxs t-muted"></i>
MyFile.png
</p>
</div>
</td>
<td rowspan="3" class="bl bkgd-dark rr-m">BKGD IMG</td>
</tr>
<tr>
<td class="bkgd-grid40 rl-m br t-size-14 t-noselect bt">
<i class="fad fa-caret-up cursor-pointer px-xxs"></i>
</td>
<td class="bkgd-gray">
<div class="px-xxs">
<p class="t-italic t-half-muted">
<i class="fa-duotone fa-solid fa-ruler-combined mr-xs"></i>32x32
<i class="fa-duotone fa-solid fa-weight-hanging mr-xs ml-m"></i>12.5 KiB
<i class="fa-duotone fa-solid fa-palette mr-xs ml-m"></i>32 bpp
<i class="fa-duotone fa-solid fa-swatchbook mr-xs ml-m"></i>256 colors
</p>
</div>
</td>
</tr>
<tr>
<td class="bkgd-grid40 rl-m br t-size-14 t-noselect">
<i class="fad fa-caret-down cursor-pointer px-xxs"></i>
</td>
<td class="bkgd-gray">
<div class="px-xxs">
<p class="t-italic t-half-muted">
<i class="fa-duotone fa-solid fa-weight-hanging mr-xs"></i>1.23 KiB
</p>
{{ render_button("<i class=\"fa-duotone fa-solid fa-gear\"></i>Options", False, None, "bkgd-blue") }}
{{ render_button("<i class=\"fa-duotone fa-solid fa-trash\"></i>Remove", False, None, "bkgd-orange") }}
{{ render_button("<i class=\"fa-duotone fa-solid fa-file-circle-info\"></i>Details", False, None, "") }}
</div>
</td>
</tr>
</table>
</div>
<div id="{{ applet_data.id }}-icon-parts-list">
<div class="border r-m">
<table class="table-p-xxs table-v-center">
<tr>
<td class="bkgd-grid40 rl-m br t-size-14 t-noselect">
<i class="fad fa-grip-vertical cursor-pointer px-xxs"></i>
</td>
<td class="w-full bkgd-gray bb">
<div class="px-xxs">
<p class="t-bold">
<i class="fa-duotone fa-solid fa-image mr-xxs t-muted"></i>
MyFile.png
</p>
</div>
</td>
<td rowspan="3" class="bl bkgd-dark rr-m">BKGD IMG</td>
</tr>
<tr>
<td class="bkgd-grid40 rl-m br t-size-14 t-noselect bt">
<i class="fad fa-caret-up cursor-pointer px-xxs"></i>
</td>
<td class="bkgd-gray">
<div class="px-xxs">
<p class="t-italic t-half-muted">
<i class="fa-duotone fa-solid fa-ruler-combined mr-xs"></i>32x32
<i class="fa-duotone fa-solid fa-weight-hanging mr-xs ml-m"></i>12.5 KiB
<i class="fa-duotone fa-solid fa-palette mr-xs ml-m"></i>32 bpp
<i class="fa-duotone fa-solid fa-swatchbook mr-xs ml-m"></i>256 colors
</p>
</div>
</td>
</tr>
<tr>
<td class="bkgd-grid40 rl-m br t-size-14 t-noselect">
<i class="fad fa-caret-down cursor-pointer px-xxs"></i>
</td>
<td class="bkgd-gray">
<div class="px-xxs">
{{ render_button("<i class=\"fa-duotone fa-solid fa-gear\"></i>Options", False, None, "bkgd-blue") }}
{{ render_button("<i class=\"fa-duotone fa-solid fa-trash\"></i>Remove", False, None, "bkgd-orange") }}
{{ render_button("<i class=\"fa-duotone fa-solid fa-file-circle-info\"></i>Details", False, None, "") }}
</div>
</td>
</tr>
</table>
</div>
<div class="border r-m">
<table class="table-p-xxs table-v-center">
<tr>
<td class="bkgd-grid40 rl-m br t-size-14 t-noselect">
<i class="fad fa-grip-vertical cursor-pointer px-xxs"></i>
</td>
<td class="w-full bkgd-gray bb">
<div class="px-xxs">
<p class="t-bold">
<i class="fa-duotone fa-solid fa-binary mr-xxs t-muted"></i>
Watermark.bin
</p>
</div>
</td>
</tr>
<tr>
<td class="bkgd-grid40 rl-m br t-size-14 t-noselect bt">
<i class="fad fa-caret-up cursor-pointer px-xxs"></i>
</td>
<td class="bkgd-gray">
<div class="px-xxs">
<p class="t-italic t-half-muted">
<i class="fa-duotone fa-solid fa-weight-hanging mr-xs"></i>12.5 KiB
</p>
</div>
</td>
</tr>
<tr>
<td class="bkgd-grid40 rl-m br t-size-14 t-noselect">
<i class="fad fa-caret-down cursor-pointer px-xxs"></i>
</td>
<td class="bkgd-gray">
<div class="px-xxs">
{{ render_button("<i class=\"fa-duotone fa-solid fa-trash\"></i>Remove", False, None, "bkgd-orange") }}
</div>
</td>
</tr>
</table>
</div>
<div class="border r-m">
<table class="table-p-xxs table-v-center">
<tr>
<td class="bkgd-grid40 rl-m br t-size-14 t-noselect">
<i class="fad fa-grip-vertical cursor-pointer px-xxs"></i>
</td>
<td class="w-full bkgd-gray bb">
<div class="px-xxs">
<p class="t-bold t-italic">
<i class="fa-duotone fa-solid fa-text-size mr-xxs t-muted"></i>
Custom Text
</p>
</div>
</td>
</tr>
<tr>
<td class="bkgd-grid40 rl-m br t-size-14 t-noselect bt">
<i class="fad fa-caret-up cursor-pointer px-xxs"></i>
</td>
<td class="bkgd-gray">
<div class="px-xxs">
<label for="test123" hidden>321</label>
<textarea name="test123" id="test123" cols="30" rows="6"
class="p-xxs border r-m w-full ta-resize-v"></textarea>
</div>
</td>
</tr>
<tr>
<td class="bkgd-grid40 rl-m br t-size-14 t-noselect">
<i class="fad fa-caret-down cursor-pointer px-xxs"></i>
</td>
<td class="bkgd-gray">
<div class="px-xxs">
{{ render_button("<i class=\"fa-duotone fa-solid fa-gear\"></i>Options", False, None, "bkgd-blue") }}
{{ render_button("<i class=\"fa-duotone fa-solid fa-trash\"></i>Remove", False, None, "bkgd-orange") }}
</div>
</td>
</tr>
</table>
</div>
<div class="border r-m">
<table class="table-p-xxs table-v-center">
<tr>
<td class="bkgd-grid40 rl-m br t-size-14 t-noselect">
<i class="fad fa-grip-vertical cursor-pointer px-xxs"></i>
</td>
<td class="w-full bkgd-gray bb">
<div class="px-xxs">
<p class="t-bold">
<i class="fa-duotone fa-solid fa-binary mr-xxs t-muted"></i>
Watermark.bin
</p>
</div>
</td>
</tr>
<tr>
<td class="bkgd-grid40 rl-m br t-size-14 t-noselect bt">
<i class="fad fa-caret-up cursor-pointer px-xxs"></i>
</td>
<td class="bkgd-gray">
<div class="px-xxs">
<p class="t-italic t-half-muted">
<i class="fa-duotone fa-solid fa-weight-hanging mr-xs"></i>12.5 KiB
</p>
</div>
</td>
</tr>
<tr>
<td class="bkgd-grid40 rl-m br t-size-14 t-noselect">
<i class="fad fa-caret-down cursor-pointer px-xxs"></i>
</td>
<td class="bkgd-gray">
<div class="px-xxs">
{{ render_button("<i class=\"fa-duotone fa-solid fa-trash\"></i>Remove", False, None, "bkgd-orange") }}
</div>
</td>
</tr>
</table>
</div>
</section>
<div class="border r-m">
<table class="table-p-xxs table-v-center">
<tr>
<td class="bkgd-grid40 rl-m br t-size-14 t-noselect">
<i class="fad fa-grip-vertical cursor-pointer px-xxs"></i>
</td>
<td class="w-full bkgd-gray bb">
<div class="px-xxs">
<p class="t-bold t-italic">
<i class="fa-duotone fa-solid fa-text-size mr-xxs t-muted"></i>
Custom Text
</p>
</div>
</td>
</tr>
<tr>
<td class="bkgd-grid40 rl-m br t-size-14 t-noselect bt">
<i class="fad fa-caret-up cursor-pointer px-xxs"></i>
</td>
<td class="bkgd-gray">
<div class="px-xxs">
<label for="test123" hidden>321</label>
<textarea name="test123" id="test123" cols="30" rows="6"
class="p-xxs border r-m w-full ta-resize-v"></textarea>
</div>
</td>
</tr>
<tr>
<td class="bkgd-grid40 rl-m br t-size-14 t-noselect">
<i class="fad fa-caret-down cursor-pointer px-xxs"></i>
</td>
<td class="bkgd-gray">
<div class="px-xxs">
{{ render_button("<i class=\"fa-duotone fa-solid fa-gear\"></i>Options", False, None, "bkgd-blue") }}
{{ render_button("<i class=\"fa-duotone fa-solid fa-trash\"></i>Remove", False, None, "bkgd-orange") }}
</div>
</td>
</tr>
</table>
</div>
</div>
<section id="{{ applet_data.id }}-licenses">
{{ render_h2(l10n("licenses.title", "commons", user_lang)) }}
{% if is_standalone %}
<hr class="subtle">
{{ render_paragraph(l10n("licenses.1", applet_data.id, user_lang)) }}
{{ render_paragraph(l10n("licenses.2", applet_data.id, user_lang)) }}
</section>
{% endif %}

View File

@@ -0,0 +1,3 @@
{{ render_file_input(tool_id + "-test-input", true, ".png", true, true) }}

View File

@@ -1,5 +0,0 @@
{{ render_h2(l10n("upload.title", "commons", user_lang)) }}
{{ render_file_input("test-input", true, None, true, true) }}