├── 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 | 
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 |
--------------------------------------------------------------------------------