├── img.png ├── LICENSE ├── README.md ├── examples.html └── epng.js /img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nildopontes/ePNG/HEAD/img.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Nildo Vieira Pontes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ePNG 2 | Um codificador PNG sem perdas, sem dependências, compacto e simples, que gera imagens de 8 bits por amostra. Todos os 5 tipos de cores (GRAYSCALE, RGB, PALETTE, GRAYSCALE ALPHA, RGBA) são suportados e o mesmo é escolhido com base na estatística das amostras. 3 | 4 | ![Imagem ilustrativa gerada com ePNG](img.png) 5 | Imagem ilustrativa gerada com ePNG 6 | 7 | #### Exemplo de uso 8 | 9 | let png = new ePNG(data, width, height [, filter]); 10 | png.encode().then(blob => { 11 | let url = window.URL.createObjectURL(blob); 12 | let img = document.createElement('img'); 13 | img.src = url; 14 | document.body.append(img); 15 | }); 16 | 17 | 18 | **data** é um array com as amostras da imagem no formato RGBA. Se a imagem não possuir transparência o canal alpha deve ser preenchido com o valor 255. 19 | 20 | **width** é a largura da imagem. 21 | 22 | **height** é a altura da imagem. 23 | 24 | **filter** é o filtro predefinido a ser aplicado à todas as scanlines. Este parâmetro aceita valores de 0 a 4. Se este não estiver no range permitido ou não fornecido será aplicada uma filtragem dinâmica conhecida como heurística da *soma mínima das diferenças absolutas*. Em imagens PALETTE o filtro será sempre 0. 25 | 26 | O método **encode()** retorna uma Promise que resolve com um Blob contendo os dados binários da imagem, que pode ser transmitido pela rede ou exibido na própria página. 27 | 28 | O codificador escolhe o tipo de cor com base na análise do canal alpha, quantidade e tipo de cores nas amostras passadas para o construtor. A compressão Zlib é fornecida pela Compression Streams API. Esta API Javascript conta com ampla compatibilidade nos navegadores modernos. 29 | 30 | Veja um exemplo comparativo de resultados com tipos de filtro diferentes. Leve em conta que o tipo de imagem influencia diretamente na eficiência da filtragem e consequente compactação. 31 | 32 | [Demonstração de uso online](https://nildopontes.com.br/ePNG/examples.html) 33 | -------------------------------------------------------------------------------- /examples.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Demo ePNG 7 | 8 | 9 | 10 |

Imagens geradas com ePNG

11 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /epng.js: -------------------------------------------------------------------------------- 1 | class ePNG { 2 | constructor(data, w, h, filter){ 3 | this.h = h; 4 | this.w = w; 5 | if(data.length != (w * h * 4)) return console.error(`Incomplete samples.`); 6 | this.data = data; 7 | this.makeCRCTable(); 8 | this.colorStatistics(); 9 | this.pixelSize = [1,,3,1,2,,4][this.colorType]; 10 | this.widthScanline = (w * this.pixelSize) + 1; 11 | this.signature = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]); 12 | this.ihdr = new Uint8Array([0, 0, 0, 13, 73, 72, 68, 82, ...this.set32bit(w), ...this.set32bit(h), 8, this.colorType, 0, 0, 0, 0, 0, 0, 0]); 13 | this.ihdr.set(this.getCRC32(this.ihdr.slice(4, 21)), 21); 14 | this.iend = new Uint8Array([0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130]); 15 | this.filter = this.colorType == 3 ? 0 : ([0, 1, 2, 3, 4].includes(filter) ? filter : 5); 16 | this.buffer = new Uint8Array((w * h * this.pixelSize) + h); 17 | } 18 | indexOf(colors, int32){ 19 | let iterator = colors.keys(); 20 | for(let i = 0; i < colors.size; i++){ 21 | if(iterator.next().value == int32) return i; 22 | } 23 | return null; 24 | } 25 | colorStatistics(){ 26 | let qtdAlpha = 0, i, colors = new Set(), transparentColors = new Set(), qtdGrayscale = 0, qtdTransparent = 0; 27 | for(i = 0; i < this.data.length; i += 4){ 28 | let int32 = this.dump32bit(this.data[i], this.data[i + 1], this.data[i + 2], this.data[i + 3]); 29 | colors.add(int32); 30 | if(this.data[i + 3] != 255) qtdAlpha++; 31 | if(this.data[i + 3] == 0){ 32 | transparentColors.add(int32); 33 | qtdTransparent++; 34 | } 35 | if(this.data[i] == this.data[i + 1] && this.data[i + 1] == this.data[i + 2]) qtdGrayscale++; 36 | } 37 | if(qtdGrayscale == this.w * this.h){ 38 | this.colorType = qtdAlpha == qtdTransparent && transparentColors.size < 2 ? 0 : 4; 39 | }else{ 40 | this.colorType = colors.size < 257 ? 3 : ((qtdAlpha == qtdTransparent && transparentColors.size < 2) ? 2 : 6); 41 | } 42 | if(transparentColors.size == 1 && this.colorType < 3 && this.indexOf(colors, transparentColors.keys().next().value + 255) != -1) this.colorType += 4; 43 | switch(this.colorType){ 44 | case 0:{ 45 | for(i = 1; i < this.data.length; i += 4){ 46 | this.data[i] = undefined; 47 | this.data[i + 1] = undefined; 48 | this.data[i + 2] = undefined; 49 | } 50 | break; 51 | } 52 | case 2:{ 53 | for(i = 3; i < this.data.length; i += 4){ 54 | this.data[i] = undefined; 55 | } 56 | break; 57 | } 58 | case 3:{ 59 | for(i = 0; i < this.data.length; i += 4){ 60 | let int32 = this.dump32bit(this.data[i], this.data[i + 1], this.data[i + 2], this.data[i + 3]); 61 | this.data[i] = this.indexOf(colors, int32); 62 | this.data[i + 1] = undefined; 63 | this.data[i + 2] = undefined; 64 | this.data[i + 3] = undefined; 65 | } 66 | this.addPLTE(colors); 67 | if(qtdAlpha > 0) this.addtRNS(colors); 68 | break; 69 | } 70 | case 4:{ 71 | for(i = 1; i < this.data.length; i += 4){ 72 | this.data[i] = undefined; 73 | this.data[i + 1] = undefined; 74 | } 75 | } 76 | } 77 | if(transparentColors.size == 1 && this.colorType < 3) this.addtRNS(transparentColors.keys().next().value); 78 | this.data = this.data.filter(sample => sample !== undefined); 79 | } 80 | addtRNS(trns){ 81 | switch(this.colorType){ 82 | case 0:{ 83 | this.trns = new Uint8Array(14); 84 | this.trns.set([2, 116, 82, 78, 83, 0, this.set32bit(trns).slice(0, 1)], 3); 85 | this.trns.set(this.getCRC32(this.trns.slice(4, 10)), 10); 86 | break; 87 | } 88 | case 2:{ 89 | this.trns = new Uint8Array(18); 90 | let rgb = this.set32bit(trns); 91 | this.trns.set([6, 116, 82, 78, 83, 0, rgb[0], 0, rgb[1], 0, rgb[2]], 3); 92 | this.trns.set(this.getCRC32(this.trns.slice(4, 14)), 14); 93 | break; 94 | } 95 | case 3:{ 96 | let iterator = trns.keys(); 97 | this.trns = new Uint8Array(trns.size + 12); 98 | this.trns.set(this.set32bit(trns.size)); 99 | this.trns.set([116, 82, 78, 83], 4); 100 | for(let i = 0; i < trns.size; i++){ 101 | this.trns[8 + i] = this.set32bit(iterator.next().value).slice(3); 102 | } 103 | this.trns.set(this.getCRC32(this.trns.slice(4, 8 + trns.size)), 8 + trns.size); 104 | break; 105 | } 106 | } 107 | } 108 | addPLTE(colors){ 109 | this.palette = new Uint8Array(colors.size * 3 + 12); 110 | this.palette.set(this.set32bit(colors.size * 3)); 111 | this.palette.set([80, 76, 84, 69], 4); 112 | let iterator = colors.keys(); 113 | for(let i = 0; i < colors.size; i++){ 114 | this.palette.set(this.set32bit(iterator.next().value).slice(0, 3), 8 + i * 3); 115 | } 116 | this.palette.set(this.getCRC32(this.palette.slice(4, 8 + colors.size * 3)), 8 + colors.size * 3); 117 | } 118 | dump32bit(a, b, c, d){ 119 | return (a << 24) + (b << 16) + (c << 8) + d; 120 | } 121 | set32bit(v){ 122 | return [v >> 24 & 255, v >> 16 & 255, v >> 8 & 255, v & 255]; 123 | } 124 | makeCRCTable(){ 125 | let c, a = []; 126 | for(let n = 0; n < 256; n++){ 127 | c = n; 128 | for(let k = 0; k < 8; k++){ 129 | c = c & 1 ? 0xEDB88320 ^ c >>> 1 : c >>> 1; 130 | } 131 | a.push(c); 132 | } 133 | this.crcTable = a; 134 | } 135 | filter0(){ 136 | this.scanlines = []; 137 | for(let i = 0; i < this.h; i++){ 138 | this.scanlines.push(new Uint8Array([0, ...this.data.slice(i * this.w * this.pixelSize, (i + 1) * this.w * this.pixelSize)])); 139 | } 140 | for(let i = this.h - 1; i >= 0; i--){ 141 | if(this.filter == 5){ 142 | let minSum = Number.MAX_VALUE, realFilter, aux = []; 143 | for(let ft = 0; ft < 5; ft++){ 144 | aux.push(this.filterScanline(ft, i, true)); 145 | if(aux[aux.length - 1][1] < minSum){ 146 | minSum = aux[aux.length - 1][1]; 147 | realFilter = ft; 148 | } 149 | } 150 | this.scanlines[i] = aux[realFilter][0]; 151 | }else{ 152 | this.scanlines[i] = this.filterScanline(this.filter, i); 153 | } 154 | } 155 | this.scanlines.map((v, i) => { 156 | this.buffer.set(v, this.widthScanline * i); 157 | }); 158 | return this.buffer; 159 | } 160 | paeth(a, b, c){ 161 | let pa = Math.abs(b - c), pb = Math.abs(a - c), pc = Math.abs(a + b - c - c); 162 | if(pb < pa){ 163 | a = b; 164 | pa = pb; 165 | } 166 | return pc < pa ? c : a; 167 | } 168 | filterScanline(type, i, returnSum){ 169 | if(this.scanlines[i] === undefined) return console.error(`Scanline ${i} non-existent.`); 170 | let filtered = new Uint8Array(this.widthScanline), j, rawA, rawB, rawC; 171 | filtered[0] = type; 172 | switch(type){ 173 | case 0:{ 174 | filtered = this.scanlines[i]; 175 | break; 176 | } 177 | case 1:{ 178 | for(j = 1; j < this.widthScanline; j++){ 179 | j < this.pixelSize + 1 ? filtered[j] = this.scanlines[i][j] : filtered[j] = this.scanlines[i][j] - this.scanlines[i][j - this.pixelSize]; 180 | } 181 | break; 182 | } 183 | case 2:{ 184 | if(i == 0){ 185 | filtered = this.scanlines[i]; 186 | }else{ 187 | for(j = 1; j < this.widthScanline; j++) filtered[j] = this.scanlines[i][j] - this.scanlines[i - 1][j]; 188 | } 189 | break; 190 | } 191 | case 3:{ 192 | for(j = 1; j < this.widthScanline; j++){ 193 | rawA = j > this.pixelSize ? this.scanlines[i][j - this.pixelSize] : 0; 194 | rawB = i > 0 ? this.scanlines[i - 1][j] : 0; 195 | filtered[j] = this.scanlines[i][j] - Math.floor((rawA + rawB) / 2); 196 | } 197 | break; 198 | } 199 | case 4:{ 200 | for(j = 1; j < this.widthScanline; j++){ 201 | rawA = j > this.pixelSize ? this.scanlines[i][j - this.pixelSize] : 0; 202 | rawB = i > 0 ? this.scanlines[i - 1][j] : 0; 203 | rawC = j > this.pixelSize && i > 0 ? this.scanlines[i - 1][j - this.pixelSize] : 0; 204 | filtered[j] = this.scanlines[i][j] - this.paeth(rawA, rawB, rawC); 205 | } 206 | } 207 | } 208 | if(returnSum){ 209 | let sum = filtered.reduce((accumulator, value) => accumulator + value) - type; 210 | return [filtered, sum]; 211 | }else{ 212 | return filtered; 213 | } 214 | } 215 | getCRC32(data){ 216 | let crc = -1; 217 | data.map(v => crc = crc >>> 8 ^ this.crcTable[(crc ^ v) & 255]); 218 | return this.set32bit(crc ^ -1 >>> 0); 219 | } 220 | compress(input){ 221 | let cs = new CompressionStream('deflate'); 222 | let writer = cs.writable.getWriter(); 223 | writer.write(input); 224 | writer.close(); 225 | return new Response(cs.readable).bytes(); 226 | } 227 | encode(){ 228 | return new Promise((resolve, reject) => { 229 | this.compress(this.filter0()).then(c => { 230 | this.idat = new Uint8Array([...this.set32bit(c.length), 73, 68, 65, 84, ...c, ...this.getCRC32([73, 68, 65, 84, ...c])]); 231 | resolve(new Blob([this.signature, this.ihdr, this.palette || [], this.trns || [], this.idat, this.iend], {type: "image/png"})); 232 | }); 233 | }); 234 | } 235 | } 236 | --------------------------------------------------------------------------------