PNG: parsing PNG data
No UI yet, but the parser seems mostly done.
|
@ -0,0 +1,28 @@
|
|||
// @ts-check
|
||||
|
||||
/**
|
||||
* @param {Uint8Array} a
|
||||
* @param {Uint8Array} b
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const areBytesEqual = (a, b) => {
|
||||
if (a.length !== b.length) return false;
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (a[i] !== b[i]) return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Uint8Array} bytes
|
||||
* @param {number} size
|
||||
* @returns {Uint8Array[]}
|
||||
*/
|
||||
export const chunkBytes = (bytes, size) => {
|
||||
/** @type {Uint8Array[]} */
|
||||
const result = [];
|
||||
for (let i = 0; i < bytes.byteLength; i += size) {
|
||||
result.push(bytes.subarray(i, i + size));
|
||||
}
|
||||
return result;
|
||||
};
|
|
@ -0,0 +1,99 @@
|
|||
// @ts-check
|
||||
|
||||
/** @enum {string} */
|
||||
export const PngNodeType = [
|
||||
"root",
|
||||
"signature",
|
||||
|
||||
"unknownChunk",
|
||||
"chunkLength",
|
||||
"chunkType",
|
||||
"chunkData",
|
||||
"chunkCrc",
|
||||
|
||||
"ihdr",
|
||||
"ihdrChunkData",
|
||||
"ihdrWidth",
|
||||
"ihdrHeight",
|
||||
"ihdrBitDepth",
|
||||
"ihdrColourType",
|
||||
"ihdrCompressionMethod",
|
||||
"ihdrFilterMethod",
|
||||
"ihdrInterlaceMethod",
|
||||
|
||||
"plte",
|
||||
"plteChunkData",
|
||||
"plteColor",
|
||||
|
||||
"idat",
|
||||
"idatChunkData",
|
||||
|
||||
"iend",
|
||||
"iendChunkLength",
|
||||
|
||||
"trns",
|
||||
|
||||
"chrm",
|
||||
|
||||
"gama",
|
||||
|
||||
"iccp",
|
||||
|
||||
"sbit",
|
||||
|
||||
"srgb",
|
||||
|
||||
"cicp",
|
||||
|
||||
"text",
|
||||
"textData",
|
||||
"textKeyword",
|
||||
"textNullSeparator",
|
||||
"textString",
|
||||
|
||||
"ztxt",
|
||||
"ztxtData",
|
||||
"ztxtCompressionMethod",
|
||||
"ztxtString",
|
||||
|
||||
"itxt",
|
||||
|
||||
"bkgd",
|
||||
|
||||
"hist",
|
||||
|
||||
"phys",
|
||||
|
||||
"splt",
|
||||
|
||||
"exif",
|
||||
|
||||
"time",
|
||||
|
||||
"actl",
|
||||
|
||||
"fctl",
|
||||
|
||||
"fdat",
|
||||
|
||||
"offs",
|
||||
|
||||
"pcal",
|
||||
|
||||
"scal",
|
||||
|
||||
"gifg",
|
||||
|
||||
"gifx",
|
||||
|
||||
"gift",
|
||||
|
||||
"ster",
|
||||
|
||||
"dsig",
|
||||
|
||||
"idot",
|
||||
].reduce((result, id) => {
|
||||
result[id] = id;
|
||||
return result;
|
||||
}, Object.create(null));
|
|
@ -0,0 +1,28 @@
|
|||
// @ts-check
|
||||
|
||||
/** * @type {number[]} */
|
||||
const crcTable = [];
|
||||
for (let i = 0; i < 256; i++) {
|
||||
let b = i;
|
||||
for (let j = 0; j < 8; j++) {
|
||||
b = b & 1 ? 0xedb88320 ^ (b >>> 1) : b >>> 1;
|
||||
}
|
||||
crcTable[i] = b >>> 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the CRC32 checksum of some `Uint8Array`s.
|
||||
* @param {Uint8Array[]} uint8arrays
|
||||
* @returns {number}
|
||||
*/
|
||||
export default (...uint8arrays) => {
|
||||
let crc = -1;
|
||||
|
||||
for (const bytes of uint8arrays) {
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
crc = crcTable[(crc ^ bytes[i]) & 0xff] ^ (crc >>> 8);
|
||||
}
|
||||
}
|
||||
|
||||
return (crc ^ -1) >>> 0;
|
||||
};
|
|
@ -11,7 +11,7 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
</head>
|
||||
<body>
|
||||
<div id="error" hidden></div>
|
||||
<div id="error" hidden>Failed to parse PNG</div>
|
||||
<div id="explorer" hidden>
|
||||
<div id="bytesEl"></div>
|
||||
<div id="treeEl"></div>
|
||||
|
|
|
@ -0,0 +1,305 @@
|
|||
// @ts-check
|
||||
|
||||
import { PngNodeType } from "./constants.js";
|
||||
import { areBytesEqual, chunkBytes } from "../common/bytes.js";
|
||||
import crc32 from "./crc32.js";
|
||||
|
||||
/** @typedef {import("../../types/png.d.ts").Node<PngNodeType>} PngNode */
|
||||
|
||||
const PNG_SIGNATURE = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]);
|
||||
|
||||
const textDecoder = new TextDecoder();
|
||||
|
||||
/**
|
||||
* @param {Uint8Array} bytes
|
||||
* @returns {null | PngNode} The root node of the PNG tree, or null if the PNG is invalid.
|
||||
*/
|
||||
export default (bytes) => {
|
||||
if (!isSignatureValid(bytes)) {
|
||||
console.warn("Invalid PNG signature");
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @type {PngNode[]} */
|
||||
const children = [{ type: PngNodeType.signature, bytes: PNG_SIGNATURE }];
|
||||
|
||||
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
||||
|
||||
for (let i = PNG_SIGNATURE.length; i < bytes.length;) {
|
||||
const lengthBytes = bytes.subarray(i, i + 4);
|
||||
if (lengthBytes.byteLength !== 4) {
|
||||
console.warn("Invalid data. Did the file end too early?");
|
||||
return null;
|
||||
}
|
||||
const length = view.getUint32(i);
|
||||
i += 4;
|
||||
|
||||
const typeBytes = bytes.subarray(i, i + 4);
|
||||
const type = parseType(typeBytes);
|
||||
if (!type) {
|
||||
console.warn("Invalid chunk type)");
|
||||
return null;
|
||||
}
|
||||
i += 4;
|
||||
|
||||
const data = bytes.subarray(i, i + length);
|
||||
if (data.length !== length) {
|
||||
console.warn("Invalid data. Did the file end too early?");
|
||||
return null;
|
||||
}
|
||||
i += length;
|
||||
|
||||
const actualCrcBytes = bytes.subarray(i, i + 4);
|
||||
if (actualCrcBytes.length !== 4) {
|
||||
console.warn("Not enough bytes for the CRC. Did the file end too early?");
|
||||
return null;
|
||||
}
|
||||
const actualCrc = view.getUint32(i);
|
||||
const expectedCrc = crc32(typeBytes, data);
|
||||
if (actualCrc !== expectedCrc) {
|
||||
console.warn("Invalid chunk CRC");
|
||||
return null;
|
||||
}
|
||||
i += 4;
|
||||
|
||||
const allBytes = bytes.subarray(i - length - 12, i);
|
||||
|
||||
children.push(
|
||||
formatChunk(allBytes, lengthBytes, typeBytes, data, actualCrcBytes),
|
||||
);
|
||||
}
|
||||
|
||||
return { type: PngNodeType.root, bytes, children };
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Uint8Array} bytes
|
||||
* @returns {boolean}
|
||||
*/
|
||||
const isSignatureValid = (bytes) =>
|
||||
areBytesEqual(bytes.subarray(0, 8), PNG_SIGNATURE);
|
||||
|
||||
/**
|
||||
* @param {Uint8Array} bytes
|
||||
* @returns {null | string} The chunk type, or null if the chunk type is invalid.
|
||||
*/
|
||||
const parseType = (bytes) => {
|
||||
if (bytes.length !== 4) return null;
|
||||
const result = textDecoder.decode(bytes);
|
||||
if (!/^[A-Za-z]{4}$/.test(result)) return null;
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Uint8Array} allBytes All the bytes in the chunk.
|
||||
* @param {Uint8Array} lengthBytes
|
||||
* @param {Uint8Array} typeBytes
|
||||
* @param {Uint8Array} data
|
||||
* @param {Uint8Array} crcBytes
|
||||
* @returns {PngNode}
|
||||
*/
|
||||
const formatChunk = (allBytes, lengthBytes, typeBytes, data, crcBytes) => {
|
||||
/** @type {PngNodeType} */
|
||||
let type = PngNodeType.unknownChunk;
|
||||
|
||||
/** @type {PngNode} */
|
||||
let chunkLengthNode = { type: PngNodeType.chunkLength, bytes: lengthBytes };
|
||||
/** @type {PngNode} */
|
||||
const chunkTypeNode = { type: PngNodeType.chunkType, bytes: typeBytes };
|
||||
/** @type {PngNode} */
|
||||
let chunkDataNode = { type: PngNodeType.chunkData, bytes: data };
|
||||
/** @type {PngNode} */
|
||||
const chunkCrcNode = { type: PngNodeType.chunkCrc, bytes: crcBytes };
|
||||
|
||||
switch (textDecoder.decode(typeBytes)) {
|
||||
case "IHDR":
|
||||
type = PngNodeType.ihdr;
|
||||
chunkDataNode = {
|
||||
...chunkDataNode,
|
||||
type: PngNodeType.ihdrChunkData,
|
||||
children: [
|
||||
{ type: PngNodeType.ihdrWidth, bytes: data.subarray(0, 4) },
|
||||
{ type: PngNodeType.ihdrHeight, bytes: data.subarray(4, 8) },
|
||||
{ type: PngNodeType.ihdrBitDepth, bytes: data.subarray(8, 9) },
|
||||
{ type: PngNodeType.ihdrColourType, bytes: data.subarray(9, 10) },
|
||||
{
|
||||
type: PngNodeType.ihdrCompressionMethod,
|
||||
bytes: data.subarray(10, 11),
|
||||
},
|
||||
{ type: PngNodeType.ihdrFilterMethod, bytes: data.subarray(11, 12) },
|
||||
{
|
||||
type: PngNodeType.ihdrInterlaceMethod,
|
||||
bytes: data.subarray(12, 13),
|
||||
},
|
||||
],
|
||||
};
|
||||
break;
|
||||
case "PLTE":
|
||||
type = PngNodeType.plte;
|
||||
chunkDataNode = {
|
||||
...chunkDataNode,
|
||||
type: PngNodeType.plteChunkData,
|
||||
children: chunkBytes(data, 3).map((bytes) => ({
|
||||
type: PngNodeType.plteColor,
|
||||
bytes,
|
||||
})),
|
||||
};
|
||||
break;
|
||||
case "IDAT":
|
||||
type = PngNodeType.idat;
|
||||
chunkDataNode = { ...chunkDataNode, type: PngNodeType.idatChunkData };
|
||||
break;
|
||||
case "IEND":
|
||||
type = PngNodeType.iend;
|
||||
chunkLengthNode = {
|
||||
...chunkLengthNode,
|
||||
type: PngNodeType.iendChunkLength,
|
||||
};
|
||||
break;
|
||||
case "tRNS":
|
||||
type = PngNodeType.trns;
|
||||
break;
|
||||
case "cHRM":
|
||||
type = PngNodeType.chrm;
|
||||
break;
|
||||
case "gAMA":
|
||||
type = PngNodeType.gama;
|
||||
break;
|
||||
case "iCCP":
|
||||
type = PngNodeType.iccp;
|
||||
break;
|
||||
case "sBIT":
|
||||
type = PngNodeType.sbit;
|
||||
break;
|
||||
case "sRGB":
|
||||
type = PngNodeType.srgb;
|
||||
break;
|
||||
case "cICP":
|
||||
type = PngNodeType.cicp;
|
||||
break;
|
||||
case "tEXt": {
|
||||
type = PngNodeType.text;
|
||||
const nullSeparatorIndex = data.indexOf(0);
|
||||
if (nullSeparatorIndex === -1) break;
|
||||
chunkDataNode = {
|
||||
...chunkDataNode,
|
||||
type: PngNodeType.textData,
|
||||
children: [
|
||||
{
|
||||
type: PngNodeType.textKeyword,
|
||||
bytes: data.subarray(0, nullSeparatorIndex),
|
||||
},
|
||||
{
|
||||
type: PngNodeType.textNullSeparator,
|
||||
bytes: data.subarray(nullSeparatorIndex, nullSeparatorIndex + 1),
|
||||
},
|
||||
{
|
||||
type: PngNodeType.textString,
|
||||
bytes: data.subarray(nullSeparatorIndex + 1),
|
||||
},
|
||||
],
|
||||
};
|
||||
break;
|
||||
}
|
||||
case "zTXt": {
|
||||
type = PngNodeType.ztxt;
|
||||
const nullSeparatorIndex = data.indexOf(0);
|
||||
if (nullSeparatorIndex === -1) break;
|
||||
chunkDataNode = {
|
||||
...chunkDataNode,
|
||||
type: PngNodeType.ztxtData,
|
||||
children: [
|
||||
{
|
||||
type: PngNodeType.textKeyword,
|
||||
bytes: data.subarray(0, nullSeparatorIndex),
|
||||
},
|
||||
{
|
||||
type: PngNodeType.textNullSeparator,
|
||||
bytes: data.subarray(nullSeparatorIndex, nullSeparatorIndex + 1),
|
||||
},
|
||||
{
|
||||
type: PngNodeType.ztxtCompressionMethod,
|
||||
bytes: data.subarray(
|
||||
nullSeparatorIndex + 1,
|
||||
nullSeparatorIndex + 2,
|
||||
),
|
||||
},
|
||||
{
|
||||
type: PngNodeType.ztxtString,
|
||||
bytes: data.subarray(nullSeparatorIndex + 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
break;
|
||||
}
|
||||
case "iTXt":
|
||||
type = PngNodeType.itxt;
|
||||
break;
|
||||
case "bKGD":
|
||||
type = PngNodeType.bkgd;
|
||||
break;
|
||||
case "hIST":
|
||||
type = PngNodeType.hist;
|
||||
break;
|
||||
case "pHYs":
|
||||
type = PngNodeType.phys;
|
||||
break;
|
||||
case "sPLT":
|
||||
type = PngNodeType.splt;
|
||||
break;
|
||||
case "eXIf":
|
||||
type = PngNodeType.exif;
|
||||
break;
|
||||
case "tIME":
|
||||
type = PngNodeType.time;
|
||||
break;
|
||||
case "acTL":
|
||||
type = PngNodeType.actl;
|
||||
break;
|
||||
case "fcTL":
|
||||
type = PngNodeType.fctl;
|
||||
break;
|
||||
case "fdAT":
|
||||
type = PngNodeType.fdat;
|
||||
break;
|
||||
case "oFFs":
|
||||
type = PngNodeType.offs;
|
||||
break;
|
||||
case "pCAL":
|
||||
type = PngNodeType.pcal;
|
||||
break;
|
||||
case "sCAL":
|
||||
type = PngNodeType.scal;
|
||||
break;
|
||||
case "gIFg":
|
||||
type = PngNodeType.gifg;
|
||||
break;
|
||||
case "gIFx":
|
||||
type = PngNodeType.gifx;
|
||||
break;
|
||||
case "gIFt":
|
||||
type = PngNodeType.gift;
|
||||
break;
|
||||
case "sTER":
|
||||
type = PngNodeType.ster;
|
||||
break;
|
||||
case "dSIG":
|
||||
type = PngNodeType.dsig;
|
||||
break;
|
||||
case "iDOT":
|
||||
type = PngNodeType.idot;
|
||||
break;
|
||||
default:
|
||||
console.warn(`Unknown chunk type ${type}`);
|
||||
break;
|
||||
}
|
||||
|
||||
/** @type {PngNode[]} */
|
||||
const children = [];
|
||||
children.push(chunkLengthNode);
|
||||
children.push(chunkTypeNode);
|
||||
if (data.byteLength) children.push(chunkDataNode);
|
||||
children.push(chunkCrcNode);
|
||||
|
||||
return { type, bytes: allBytes, children };
|
||||
};
|
|
@ -1,5 +1,7 @@
|
|||
// @ts-check
|
||||
|
||||
import parsePng from "./parsePng.js";
|
||||
|
||||
import parseHash from "./parseHash.js";
|
||||
|
||||
const errorEl = document.getElementById("error");
|
||||
|
@ -14,8 +16,17 @@ const main = () => {
|
|||
return;
|
||||
}
|
||||
|
||||
const { bytes } = parsedHash;
|
||||
|
||||
const rootNode = parsePng(bytes);
|
||||
if (!rootNode) {
|
||||
// TODO: Is there better UI than this?
|
||||
errorEl.removeAttribute("hidden");
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Actually do something!
|
||||
console.log(parsedHash);
|
||||
console.log(rootNode);
|
||||
};
|
||||
|
||||
main();
|
||||
|
|
|
@ -1,11 +1,7 @@
|
|||
import { assertEquals } from "assert";
|
||||
import { b } from "../helpers.ts";
|
||||
import * as base64url from "../../public/common/base64url.js";
|
||||
|
||||
/**
|
||||
* A shorthand for `new Uint8Array()`.
|
||||
*/
|
||||
const b = (...bytes: number[]) => new Uint8Array(bytes);
|
||||
|
||||
Deno.test('encodes no bytes as ""', () => {
|
||||
assertEquals(base64url.stringify(b()), "");
|
||||
});
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
import { assert, assertEquals } from "assert";
|
||||
import { b } from "../helpers.ts";
|
||||
import { areBytesEqual, chunkBytes } from "../../public/common/bytes.js";
|
||||
|
||||
Deno.test("areBytesEqual", () => {
|
||||
const x = b(1, 2);
|
||||
const y = b(1, 2, 3);
|
||||
const z = b(1, 2, 3);
|
||||
|
||||
assert(areBytesEqual(x, x));
|
||||
assert(areBytesEqual(y, z));
|
||||
assert(!areBytesEqual(x, y));
|
||||
});
|
||||
|
||||
Deno.test("chunkBytes", () => {
|
||||
assertEquals(
|
||||
chunkBytes(b(1, 2, 3, 4, 5, 6, 7, 8), 3),
|
||||
[b(1, 2, 3), b(4, 5, 6), b(7, 8)],
|
||||
);
|
||||
});
|
|
@ -0,0 +1,25 @@
|
|||
import { stub } from "mock";
|
||||
|
||||
/**
|
||||
* A shorthand for `new Uint8Array()`.
|
||||
*/
|
||||
export const b = (...bytes: number[]) => new Uint8Array(bytes);
|
||||
|
||||
/**
|
||||
* @example
|
||||
* Deno.test("works", stubbingWarn(() => {
|
||||
* // ...
|
||||
* }));
|
||||
*/
|
||||
export const stubbingWarn = (
|
||||
fn: (t: Deno.TestContext) => void | Promise<void>,
|
||||
): (t: Deno.TestContext) => Promise<void> => (
|
||||
async (t) => {
|
||||
const warnStub = stub(console, "warn");
|
||||
try {
|
||||
return await fn(t);
|
||||
} finally {
|
||||
warnStub.restore();
|
||||
}
|
||||
}
|
||||
);
|
|
@ -0,0 +1,21 @@
|
|||
import { assertEquals } from "assert";
|
||||
import { b } from "../helpers.ts";
|
||||
import crc32 from "../../public/png/crc32.js";
|
||||
|
||||
Deno.test("computes CRC32s", () => {
|
||||
// These tests are lifted from the following Go 1.19.3 code:
|
||||
//
|
||||
// ```
|
||||
// crc := crc32.NewIEEE()
|
||||
// fmt.Println(crc.Sum32())
|
||||
// crc.Write([]byte{})
|
||||
// fmt.Println(crc.Sum32())
|
||||
// crc.Write([]byte{1, 2, 3})
|
||||
// fmt.Println(crc.Sum32())
|
||||
// crc.Write([]byte{4, 5, 6})
|
||||
// fmt.Println(crc.Sum32())
|
||||
// ```
|
||||
assertEquals(crc32(), 0);
|
||||
assertEquals(crc32(b(1, 2, 3)), 1438416925);
|
||||
assertEquals(crc32(b(1, 2, 3, 4, 5, 6)), 2180413220);
|
||||
});
|
After Width: | Height: | Size: 217 B |
After Width: | Height: | Size: 154 B |
After Width: | Height: | Size: 247 B |
After Width: | Height: | Size: 254 B |
After Width: | Height: | Size: 299 B |
After Width: | Height: | Size: 315 B |
After Width: | Height: | Size: 595 B |
After Width: | Height: | Size: 132 B |
After Width: | Height: | Size: 193 B |
After Width: | Height: | Size: 327 B |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 214 B |
After Width: | Height: | Size: 2.8 KiB |
After Width: | Height: | Size: 361 B |
After Width: | Height: | Size: 4.1 KiB |
After Width: | Height: | Size: 164 B |
After Width: | Height: | Size: 104 B |
After Width: | Height: | Size: 145 B |
After Width: | Height: | Size: 138 B |
After Width: | Height: | Size: 167 B |
After Width: | Height: | Size: 145 B |
After Width: | Height: | Size: 302 B |
After Width: | Height: | Size: 112 B |
After Width: | Height: | Size: 146 B |
After Width: | Height: | Size: 216 B |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 126 B |
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 184 B |
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 214 B |
After Width: | Height: | Size: 2.8 KiB |
After Width: | Height: | Size: 184 B |
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 140 B |
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 202 B |
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 404 B |
After Width: | Height: | Size: 344 B |
After Width: | Height: | Size: 232 B |
After Width: | Height: | Size: 724 B |
After Width: | Height: | Size: 258 B |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 292 B |
After Width: | Height: | Size: 292 B |
After Width: | Height: | Size: 292 B |
After Width: | Height: | Size: 214 B |
After Width: | Height: | Size: 259 B |
After Width: | Height: | Size: 186 B |
After Width: | Height: | Size: 271 B |
After Width: | Height: | Size: 149 B |
After Width: | Height: | Size: 256 B |
After Width: | Height: | Size: 273 B |
After Width: | Height: | Size: 792 B |
After Width: | Height: | Size: 742 B |
After Width: | Height: | Size: 716 B |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 941 B |
After Width: | Height: | Size: 753 B |
After Width: | Height: | Size: 1.7 KiB |
After Width: | Height: | Size: 319 B |
After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 321 B |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 355 B |
After Width: | Height: | Size: 1.7 KiB |
After Width: | Height: | Size: 389 B |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 269 B |
After Width: | Height: | Size: 985 B |
After Width: | Height: | Size: 426 B |
After Width: | Height: | Size: 345 B |
After Width: | Height: | Size: 370 B |
After Width: | Height: | Size: 214 B |
After Width: | Height: | Size: 363 B |
After Width: | Height: | Size: 377 B |
After Width: | Height: | Size: 219 B |
After Width: | Height: | Size: 339 B |
After Width: | Height: | Size: 350 B |
After Width: | Height: | Size: 206 B |
After Width: | Height: | Size: 321 B |
After Width: | Height: | Size: 340 B |
After Width: | Height: | Size: 207 B |
After Width: | Height: | Size: 262 B |
After Width: | Height: | Size: 285 B |
After Width: | Height: | Size: 214 B |