diff --git a/data/applets/png-analyser.yml b/data/applets/png-analyser.yml new file mode 100644 index 0000000..898d5a8 --- /dev/null +++ b/data/applets/png-analyser.yml @@ -0,0 +1,7 @@ +applets: + - id: "png-analyser" + resources: + scripts: + - "applet://png-analyser.mjs" + stylesheets: + - "applet://png-analyser.css" diff --git a/data/applets/png-chunk-analyser.yml b/data/applets/png-chunk-analyser.yml deleted file mode 100644 index 6384dff..0000000 --- a/data/applets/png-chunk-analyser.yml +++ /dev/null @@ -1,7 +0,0 @@ -applets: - - id: "png-chunk-analyser" - resources: - scripts: - - "applet://png-chunk-analyser.mjs" - stylesheets: - - "applet://png-chunk-analyser.css" diff --git a/data/strings/en/ico-maker.yml b/data/strings/en/ico-maker.yml index a23e215..9140da3 100644 --- a/data/strings/en/ico-maker.yml +++ b/data/strings/en/ico-maker.yml @@ -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" diff --git a/data/strings/fr/ico-maker.yml b/data/strings/fr/ico-maker.yml index 3f85b63..d98eb28 100644 --- a/data/strings/fr/ico-maker.yml +++ b/data/strings/fr/ico-maker.yml @@ -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" diff --git a/data/tools/.ico-maker.yml b/data/tools/ico-maker.yml similarity index 92% rename from data/tools/.ico-maker.yml rename to data/tools/ico-maker.yml index ef21163..bbe31b3 100644 --- a/data/tools/.ico-maker.yml +++ b/data/tools/ico-maker.yml @@ -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" diff --git a/data/tools/.png-chunk-analyser.yml b/data/tools/png-analyser.yml similarity index 93% rename from data/tools/.png-chunk-analyser.yml rename to data/tools/png-analyser.yml index e140185..e5e0e8f 100644 --- a/data/tools/.png-chunk-analyser.yml +++ b/data/tools/png-analyser.yml @@ -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" diff --git a/static/resources/NibblePoker/applets/ico-maker/ico-maker.css b/static/resources/NibblePoker/applets/ico-maker/ico-maker.css index e69de29..b2c0ddd 100644 --- a/static/resources/NibblePoker/applets/ico-maker/ico-maker.css +++ b/static/resources/NibblePoker/applets/ico-maker/ico-maker.css @@ -0,0 +1,8 @@ + +.ico-maker-advanced { + display: none; +} + +input[type="checkbox"]#ico-maker-enable-expert-mode:checked ~ .ico-maker-advanced { + display: block; +} diff --git a/static/resources/NibblePoker/applets/png-chunk-analyser/png-chunk-analyser.css b/static/resources/NibblePoker/applets/png-analyser/png-analyser.css similarity index 100% rename from static/resources/NibblePoker/applets/png-chunk-analyser/png-chunk-analyser.css rename to static/resources/NibblePoker/applets/png-analyser/png-analyser.css diff --git a/static/resources/NibblePoker/applets/png-analyser/png-analyser.mjs b/static/resources/NibblePoker/applets/png-analyser/png-analyser.mjs new file mode 100644 index 0000000..66364cd --- /dev/null +++ b/static/resources/NibblePoker/applets/png-analyser/png-analyser.mjs @@ -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()); + }); + }); + }; +} diff --git a/static/resources/NibblePoker/applets/png-chunk-analyser/png-chunk-analyser.mjs b/static/resources/NibblePoker/applets/png-chunk-analyser/png-chunk-analyser.mjs deleted file mode 100644 index e69de29..0000000 diff --git a/static/resources/NibblePoker/libs/data-utils.mjs b/static/resources/NibblePoker/libs/data-utils.mjs new file mode 100644 index 0000000..aafca04 --- /dev/null +++ b/static/resources/NibblePoker/libs/data-utils.mjs @@ -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); +} diff --git a/static/resources/NibblePoker/libs/file-utils.mjs b/static/resources/NibblePoker/libs/file-utils.mjs new file mode 100644 index 0000000..e32e3c7 --- /dev/null +++ b/static/resources/NibblePoker/libs/file-utils.mjs @@ -0,0 +1,28 @@ + +/** + * Reads a file and returns its content as a string. + * @param {File} file - The file to read. + * @returns {Promise} 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} 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); + }); +} diff --git a/static/resources/NibblePoker/libs/png-utils.mjs b/static/resources/NibblePoker/libs/png-utils.mjs new file mode 100644 index 0000000..7485557 --- /dev/null +++ b/static/resources/NibblePoker/libs/png-utils.mjs @@ -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} A promise that resolves with parsed PNG file. + */ +export function parsePngFile(file) { + return loadFileAsUint8Array(file).then(byteBuffer => { + return new PngFile(file, byteBuffer); + }); +} diff --git a/templates/applets/ico-maker.jinja b/templates/applets/ico-maker.jinja index d5401a5..aa38e88 100644 --- a/templates/applets/ico-maker.jinja +++ b/templates/applets/ico-maker.jinja @@ -1,26 +1,14 @@ -
- {{ 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)) }} -
+{{ render_file_input(applet_data.id + "-image-input-file", true, ".png, .bmp, .ico", true, false, user_lang) }} -
- {{ render_h2(l10n("input.title", "commons", user_lang)) }} - {{ render_paragraph(l10n("input.1", applet_data.id, user_lang)) }} +
- {{ render_file_input(applet_data.id + "-image-input-file", true, ".png, .bmp, .ico", true, false, user_lang) }} - -
- - - - - -
+ + +
@@ -35,176 +23,174 @@ {{ render_button("Remove Binary Blobs", False, None, "bkgd-orange") }} - {{ render_button("Remove Everything", False, None, "bkgd-red") }} + -
+ -
- {{ render_h2(l10n("idk01.title", "commons", user_lang)) }} - {{ render_paragraph(l10n("idk01.1", applet_data.id, user_lang)) }} +
- {{ render_button("Download .ico", False, None, "bkgd-green") }} + +{{ render_button("Download .ico", False, None, "bkgd-green") }} + +
+ + + + + + + + +
+ + +
+

Required ICO Headers

+
+
+
+

+ 1.23 KiB +

+
+
+
+ +
- +
- - + + + + + +
- + + +
-

Required ICO Headers

+

+ + MyFile.png +

+
+
BKGD IMG
+ + +
+

+ 32x32 + 12.5 KiB + 32 bpp + 256 colors +

+ +
-

- 1.23 KiB -

+ {{ render_button("Options", False, None, "bkgd-blue") }} + {{ render_button("Remove", False, None, "bkgd-orange") }} + {{ render_button("Details", False, None, "") }}
-
- -
- - - - - - - - - - - - - - -
- - -
-

- - MyFile.png -

-
-
BKGD IMG
- - -
-

- 32x32 - 12.5 KiB - 32 bpp - 256 colors -

-
-
- - -
- {{ render_button("Options", False, None, "bkgd-blue") }} - {{ render_button("Remove", False, None, "bkgd-orange") }} - {{ render_button("Details", False, None, "") }} -
-
-
- -
- - - - - - - - - - - - - -
- - -
-

- - Watermark.bin -

-
-
- - -
-

- 12.5 KiB -

-
-
- - -
- {{ render_button("Remove", False, None, "bkgd-orange") }} -
-
-
- -
- - - - - - - - - - - - - -
- - -
-

- - Custom Text -

-
-
- - -
- - -
-
- - -
- {{ render_button("Options", False, None, "bkgd-blue") }} - {{ render_button("Remove", False, None, "bkgd-orange") }} -
-
-
- +
+ + + + + + + + + + + + + +
+ + +
+

+ + Watermark.bin +

+
+
+ + +
+

+ 12.5 KiB +

+
+
+ + +
+ {{ render_button("Remove", False, None, "bkgd-orange") }} +
+
-
+
+ + + + + + + + + + + + + +
+ + +
+

+ + Custom Text +

+
+
+ + +
+ + +
+
+ + +
+ {{ render_button("Options", False, None, "bkgd-blue") }} + {{ render_button("Remove", False, None, "bkgd-orange") }} +
+
+
+ + -
- {{ render_h2(l10n("licenses.title", "commons", user_lang)) }} + +{% if is_standalone %} +
{{ render_paragraph(l10n("licenses.1", applet_data.id, user_lang)) }} {{ render_paragraph(l10n("licenses.2", applet_data.id, user_lang)) }} -
+{% endif %} diff --git a/templates/applets/png-analyser.jinja b/templates/applets/png-analyser.jinja new file mode 100644 index 0000000..78b6606 --- /dev/null +++ b/templates/applets/png-analyser.jinja @@ -0,0 +1,3 @@ + +{{ render_file_input(tool_id + "-test-input", true, ".png", true, true) }} + diff --git a/templates/applets/png-chunk-analyser.jinja b/templates/applets/png-chunk-analyser.jinja deleted file mode 100644 index 127233e..0000000 --- a/templates/applets/png-chunk-analyser.jinja +++ /dev/null @@ -1,5 +0,0 @@ - -{{ render_h2(l10n("upload.title", "commons", user_lang)) }} - -{{ render_file_input("test-input", true, None, true, true) }} -