Check if file is valid (A)PNG image
(c) Copyright, all rights reserved
by PhilQ
HTML
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.1/css/all.min.css">
<label for="file">
<input type="file" id="file" class="visually-hidden" accept="image/*" />
<p class="pick">
<i class="fa-solid fa-folder"></i>
<i class="fa-regular fa-folder-open"></i>
<span>Pick an image</span>
</p>
<p class="is-png">
<i class="fa-solid fa-file-image"></i>
<!-- <i class="fa-solid fa-image"></i> -->
<span>filename.png</span>
</p>
<p class="is-apng">
<i class="fa-solid fa-file-image"></i>
<!-- <i class="fa-solid fa-film"></i> -->
<span>filename.apng</span>
</p>
<p class="not-png">
<i class="fa-solid fa-file"></i>
<span>filename.ext</span>
</p>
</label>
<div class="wrapper">
<div class="col-left">
<ul id="info">
<!--
<li><i class="fa-solid fa-arrows-left-right" title="Width"></i>- px</li>
<li><i class="fa-solid fa-arrows-up-down" title="Height"></i>- px</li>
-->
<li><i class="fa-solid fa-ruler-combined" title="Dimensions - Width x Height"></i> </li>
<li><i class="fa-solid fa-palette" title="Color type"></i>-</li>
<li><i class="fa-solid fa-droplet" title="Bit depth"></i>- bit</li>
<li><i class="fa-solid fa-film" title="Frames"></i>- frames</li>
<li><i class="fa-solid fa-file-zipper" title="Compression method"></i>Deflate</li>
<li><i class="fa-solid fa-filter" title="Filter method"></i> </li>
<li><i class="fa-solid fa-shuffle" title="Interlace method"></i> </li>
</ul>
<ul id="refs">
<li><a target="_blank" rel="noreferrer noopener" href="https://www.w3.org/TR/png/"><i class="fa-solid fa-file-lines"></i>(A)PNG specification</a> <sup>latest draft</sup></li>
<li><a target="_blank" rel="noreferrer noopener" href="http://www.libpng.org/pub/png/spec/iso/index-object.html"><i class="fa-solid fa-file-circle-check"></i>PNG specification</a> <sup>2nd edition</sup></li>
<li><a target="_blank" rel="noreferrer noopener" href="https://wiki.mozilla.org/APNG_Specification"><i...
SCSS
*, ::before, ::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
@import url('https://fonts.cdnfonts.com/css/cascadia-code');
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,600;1,600&family=Source+Sans+Pro:ital,wght@0,400;0,700;1,400;1,700&display=swap');
html {
width: 100%;
height: 100%;
}
body {
display: flex;
flex-direction: column;
gap: 4em;
width: 100%;
min-height: 100%;
font-family: 'Source Code Sans', 'Segoe UI', Tahoma, Geneva, Verdana,
sans-serif;
background: #f1f1f1;
padding: 4em;
}
code {
font-family: 'Cascadia Code', sans-serif;
// font-family: 'JetBrains Mono', monospace;
}
/* See: https://www.a11yproject.com/posts/how-to-hide-content/ */
.visually-hidden {
/*
clip: rect(0 0 0 0);
clip-path: inset(50%);
white-space: nowrap;
width: 1px;
height: 1px;
overflow: hidden;
*/
position: absolute;
width: 300%;
height: 300%;
opacity: 1;
top: -200%;
left: -200%;
cursor: pointer;
z-index: 1;
outline: none;
}
label {
position: relative;
display: block;
width: 100%;
max-width: 450px;
margin: 0 auto;
background: #fff;
border-radius: 0.5em;
cursor: pointer;
height: 6rem;
font-size: 1.75em;
outline: none;
-webkit-user-select: none; /* Safari */
-ms-user-select: none; /* IE 10 and IE 11 */
user-select: none; /* Standard syntax */
overflow: hidden;
p {
display: none;
position: relative;
width: 100%;
height: 100%;
align-items: center;
}
i {
padding-left: 0.7em;
font-size: 2.5rem;
color: #ccc;
flex-basis: 6rem;
flex-shrink: 0;
}
span {
font-weight: bold;
color: hsl(216, 10, 40);
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
padding-right: 1em;
padding-bottom: 0.15em;
}
&:focus-within, &:hover {
i {
color: hsl(216, 80, 75);
}
span {
color: hsl(216, 80, 40);
}
}
&:not(.is-png):not(.is-apng):not(.not-png) {
display: flex;
.pick {
display: flex;
}
.pick .fa-folder-open...
TypeScript
// Decimal <-> Hex
const d2h = (d: number, l = 2): string => d.toString(16).padStart(l, '0')
const h2d = (h: string): number => Number.parseInt('0x' + h)
// Decimal <-> Binary
const d2b = (d: number, l = 2): string => d.toString(2).padStart(l, '0')
const b2d = (b: string): number => Number.parseInt('0b' + b)
// Decimal <-> ASCII
const d2a = (d: number): string => String.fromCharCode(d)
// const a2d = (a: string): number => a.charCodeAt(0)
// Hex <-> Binary
// const h2b = (h: string, l: number = 0): string => d2b(h2d(h), l || h.length);
// const b2h = (b: string, l: number = 0): string => d2h(b2d(b), l || b.length);
/*
PNG specs
see: http://www.libpng.org/pub/png/spec/iso/
see: http://www.libpng.org/pub/png/spec/1.2/
APNG specs
see: https://wiki.mozilla.org/APNG_Specification
see: https://en.wikipedia.org/wiki/APNG
*/
const PNG_SIGNATURE_LENGTH = 8
const PNG_CHUNK_OVERHEAD_LENGTH = 4 + 4 + 4 // length, type and crc
const APNG_FRAME_CHUNK_TYPES = ['fcTL', 'IDAT', 'fdAT'] as const
type ReadPNGOptions = {
onRead?: (state: {
chunk: ChunkInfo
data: Uint8Array
offsetAfter: number
png: PNGInfo
}) => void
forceWithData?: boolean
forceWithCRC?: boolean
}
type PNGInfo = {
width: number
height: number
color_type: number
bit_depth: number
animated: boolean
frames: number
thumbnail: boolean
count: {
[chunkType: string]: number
}
valid: {
[checkNameOrChunkType: string]: boolean | undefined
dimensions?: boolean
criticalChunks?: boolean
frameCount?: boolean
}
loops?: number
isValid: boolean
}
type ChunkInfo = {
length: number
type: {
h: string
b: string
a: string
}
data: Uint8Array | null
crc: number
content?: {
[key: string]: string | boolean | number
}
}
// type ChunkInfoWithData = ChunkInfo & {
// data: Uint8Array
// }
type ChunkInterpreter = {
callback: (chunk: ChunkInfo, png: PNGInfo) => void
needsData: boolean
needsCRC: boolean
}
// | {
// ...