Skip to content

Commit

Permalink
Optimal PNG tag repacking
Browse files Browse the repository at this point in the history
  • Loading branch information
JrMasterModelBuilder committed Sep 24, 2023
1 parent 866369a commit b9ef636
Show file tree
Hide file tree
Showing 4 changed files with 104 additions and 8 deletions.
43 changes: 40 additions & 3 deletions src/icon.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {PNG} from 'pngjs';

import {IImageData} from './types';
import {concatUint8Arrays} from './util';
import {concatUint8Arrays, pngReader, pngEncode} from './util';

/**
* Icon object.
Expand Down Expand Up @@ -44,9 +44,13 @@ export abstract class Icon {
* Encode RGBA image to PNG data.
*
* @param imageData Image data.
* @param srgb SRGB mode.
* @returns PNG data.
*/
protected async _encodeRgbaToPng(imageData: Readonly<IImageData>) {
protected async _encodeRgbaToPng(
imageData: Readonly<IImageData>,
srgb: number | null = null
) {
const {width, height, data} = imageData;
const png = new PNG({
width,
Expand All @@ -65,7 +69,40 @@ export abstract class Icon {
png.on('end', resolve);
png.pack();
});
return concatUint8Arrays(packed);
let ihdr: Uint8Array | null = null;
let iend: Uint8Array | null = null;
const idats = [];
for (const [tag, data] of pngReader(concatUint8Arrays(packed))) {
switch (tag) {
// IDAT
case 0x49444154: {
idats.push(data);
break;
}
// IHDR
case 0x49484452: {
ihdr = data;
break;
}
// IEND
case 0x49454e44: {
iend = data;
break;
}
default: {
// Discard others.
}
}
}
if (!ihdr || !iend) {
throw new Error('Encode error');
}
const pieces: [number, Uint8Array][] = [[0x49484452, ihdr]];
if (srgb !== null) {
pieces.push([0x73524742, new Uint8Array([srgb])]);
}
pieces.push([0x49444154, concatUint8Arrays(idats)], [0x49454e44, iend]);
return pngEncode(pieces);
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/icon/icns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ export class IconIcns extends Icon {
imageData: Readonly<IImageData>,
_type: string
) {
return this._encodeRgbaToPng(imageData);
return this._encodeRgbaToPng(imageData, 0);
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/icon/ico.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export class IconIco extends Icon {
width: imageData.width,
height: imageData.height,
data: isPng
? await this._encodeRgbaToPng(imageData)
? await this._encodeRgbaToPng(imageData, 0)
: this._encodeRgbaToBmp(imageData)
});
}
Expand Down
65 changes: 62 additions & 3 deletions src/util.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,36 @@
import {IPngIhdr} from './types';

const PNG_MAGIC = [137, 80, 78, 71, 13, 10, 26, 10];
const PNG_IDHR = 0x49484452;
const PNG_MAGIC_SIZE = 8;

const CRC32_TABLE: number[] = [];

/**
* Hash data with CRC32.
*
* @param data Data to be hashed.
* @returns Data hash.
*/
export function crc32(data: Readonly<Uint8Array>) {
if (!CRC32_TABLE.length) {
for (let i = 256; i--; ) {
let c = i;
for (let j = 0; j < 8; j++) {
// eslint-disable-next-line no-bitwise
c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
}
CRC32_TABLE[i] = c;
}
}
let r = -1;
const l = data.length;
for (let i = 0; i < l; ) {
// eslint-disable-next-line no-bitwise
r = CRC32_TABLE[(r ^ data[i++]) & 0xff] ^ (r >>> 8);
}
// eslint-disable-next-line no-bitwise
return r ^ -1;
}

/**
* Concatenate multiple Uint8Array together.
Expand Down Expand Up @@ -31,7 +60,7 @@ export function concatUint8Arrays(arrays: Readonly<Readonly<Uint8Array>[]>) {
*/
export function* pngReader(data: Readonly<Uint8Array>) {
let i = 0;
for (; i < 8; i++) {
for (; i < PNG_MAGIC_SIZE; i++) {
if (data[i] !== PNG_MAGIC[i]) {
throw new Error('Invalid PNG header signature');
}
Expand All @@ -51,6 +80,36 @@ export function* pngReader(data: Readonly<Uint8Array>) {
}
}

/**
* Encode PNG from tags.
*
* @param tags PNG tags and data.
* @returns PNG data.
*/
export function pngEncode(tags: Readonly<[number, Readonly<Uint8Array>][]>) {
let total = PNG_MAGIC_SIZE;
for (const [, data] of tags) {
total += 12 + data.length;
}
const r = new Uint8Array(total);
const d = new DataView(r.buffer, r.byteOffset, r.byteLength);
r.set(PNG_MAGIC);
let i = PNG_MAGIC_SIZE;
for (const [tag, data] of tags) {
const l = data.length;
d.setUint32(i, l, false);
i += 4;
const f = i;
d.setUint32(i, tag, false);
i += 4;
r.set(data, i);
i += l;
d.setUint32(i, crc32(r.subarray(f, i)), false);
i += 4;
}
return r;
}

/**
* Read PNG IHDR data.
*
Expand All @@ -59,7 +118,7 @@ export function* pngReader(data: Readonly<Uint8Array>) {
*/
export function pngIhdr(data: Readonly<Uint8Array>): IPngIhdr {
for (const [tag, td] of pngReader(data)) {
if (tag === PNG_IDHR) {
if (tag === 0x49484452) {
const d = new DataView(td.buffer, td.byteOffset, td.byteLength);
return {
width: d.getUint32(0, false),
Expand Down

0 comments on commit b9ef636

Please sign in to comment.