1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.apache.commons.imaging.formats.png;
18
19 import java.awt.image.BufferedImage;
20 import java.io.ByteArrayOutputStream;
21 import java.io.IOException;
22 import java.io.OutputStream;
23 import java.nio.charset.StandardCharsets;
24 import java.util.List;
25 import java.util.zip.Deflater;
26 import java.util.zip.DeflaterOutputStream;
27
28 import org.apache.commons.imaging.ImagingException;
29 import org.apache.commons.imaging.PixelDensity;
30 import org.apache.commons.imaging.common.Allocator;
31 import org.apache.commons.imaging.internal.Debug;
32 import org.apache.commons.imaging.palette.Palette;
33 import org.apache.commons.imaging.palette.PaletteFactory;
34
35 public class PngWriter {
36
37
38
39
40
41
42
43
44
45
46
47
48 private static final class ImageHeader {
49 public final int width;
50 public final int height;
51 public final byte bitDepth;
52 public final PngColorType pngColorType;
53 public final byte compressionMethod;
54 public final byte filterMethod;
55 public final InterlaceMethod interlaceMethod;
56
57 ImageHeader(final int width, final int height, final byte bitDepth, final PngColorType pngColorType, final byte compressionMethod,
58 final byte filterMethod, final InterlaceMethod interlaceMethod) {
59 this.width = width;
60 this.height = height;
61 this.bitDepth = bitDepth;
62 this.pngColorType = pngColorType;
63 this.compressionMethod = compressionMethod;
64 this.filterMethod = filterMethod;
65 this.interlaceMethod = interlaceMethod;
66 }
67
68 }
69
70 private byte[] deflate(final byte[] bytes) throws IOException {
71 try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
72 try (DeflaterOutputStream dos = new DeflaterOutputStream(baos)) {
73 dos.write(bytes);
74
75 }
76 return baos.toByteArray();
77 }
78 }
79
80 private byte getBitDepth(final PngColorType pngColorType, final PngImagingParameters params) {
81 final byte depth = params.getBitDepth();
82
83 return pngColorType.isBitDepthAllowed(depth) ? depth : PngImagingParameters.DEFAULT_BIT_DEPTH;
84 }
85
86 private boolean isValidISO_8859_1(final String s) {
87 final String roundtrip = new String(s.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.ISO_8859_1);
88 return s.equals(roundtrip);
89 }
90
91 private void writeChunk(final OutputStream os, final ChunkType chunkType, final byte[] data) throws IOException {
92 final int dataLength = data == null ? 0 : data.length;
93 writeInt(os, dataLength);
94 os.write(chunkType.array);
95 if (data != null) {
96 os.write(data);
97 }
98
99 final PngCrc pngCrc = new PngCrc();
100
101 final long crc1 = pngCrc.startPartialCrc(chunkType.array, chunkType.array.length);
102 final long crc2 = data == null ? crc1 : pngCrc.continuePartialCrc(crc1, data, data.length);
103 final int crc = (int) pngCrc.finishPartialCrc(crc2);
104
105 writeInt(os, crc);
106 }
107
108 private void writeChunkIDAT(final OutputStream os, final byte[] bytes) throws IOException {
109 writeChunk(os, ChunkType.IDAT, bytes);
110 }
111
112 private void writeChunkIEND(final OutputStream os) throws IOException {
113 writeChunk(os, ChunkType.IEND, null);
114 }
115
116 private void writeChunkIHDR(final OutputStream os, final ImageHeader value) throws IOException {
117 final ByteArrayOutputStream baos = new ByteArrayOutputStream();
118 writeInt(baos, value.width);
119 writeInt(baos, value.height);
120 baos.write(0xff & value.bitDepth);
121 baos.write(0xff & value.pngColorType.getValue());
122 baos.write(0xff & value.compressionMethod);
123 baos.write(0xff & value.filterMethod);
124 baos.write(0xff & value.interlaceMethod.ordinal());
125
126 writeChunk(os, ChunkType.IHDR, baos.toByteArray());
127 }
128
129 private void writeChunkiTXt(final OutputStream os, final AbstractPngText.Itxt text) throws IOException, ImagingException {
130 if (!isValidISO_8859_1(text.keyword)) {
131 throw new ImagingException("PNG tEXt chunk keyword is not ISO-8859-1: " + text.keyword);
132 }
133 if (!isValidISO_8859_1(text.languageTag)) {
134 throw new ImagingException("PNG tEXt chunk language tag is not ISO-8859-1: " + text.languageTag);
135 }
136
137 final ByteArrayOutputStream baos = new ByteArrayOutputStream();
138
139
140 baos.write(text.keyword.getBytes(StandardCharsets.ISO_8859_1));
141 baos.write(0);
142
143 baos.write(1);
144 baos.write(PngConstants.COMPRESSION_DEFLATE_INFLATE);
145
146
147 baos.write(text.languageTag.getBytes(StandardCharsets.ISO_8859_1));
148 baos.write(0);
149
150
151 baos.write(text.translatedKeyword.getBytes(StandardCharsets.UTF_8));
152 baos.write(0);
153
154 baos.write(deflate(text.text.getBytes(StandardCharsets.UTF_8)));
155
156 writeChunk(os, ChunkType.iTXt, baos.toByteArray());
157 }
158
159 private void writeChunkPHYS(final OutputStream os, final int xPPU, final int yPPU, final byte units) throws IOException {
160 final byte[] bytes = new byte[9];
161 bytes[0] = (byte) (0xff & xPPU >> 24);
162 bytes[1] = (byte) (0xff & xPPU >> 16);
163 bytes[2] = (byte) (0xff & xPPU >> 8);
164 bytes[3] = (byte) (0xff & xPPU >> 0);
165 bytes[4] = (byte) (0xff & yPPU >> 24);
166 bytes[5] = (byte) (0xff & yPPU >> 16);
167 bytes[6] = (byte) (0xff & yPPU >> 8);
168 bytes[7] = (byte) (0xff & yPPU >> 0);
169 bytes[8] = units;
170 writeChunk(os, ChunkType.pHYs, bytes);
171 }
172
173 private void writeChunkPLTE(final OutputStream os, final Palette palette) throws IOException {
174 final int length = palette.length();
175 final byte[] bytes = Allocator.byteArray(length * 3);
176
177
178 for (int i = 0; i < length; i++) {
179 final int rgb = palette.getEntry(i);
180 final int index = i * 3;
181
182 bytes[index + 0] = (byte) (0xff & rgb >> 16);
183 bytes[index + 1] = (byte) (0xff & rgb >> 8);
184 bytes[index + 2] = (byte) (0xff & rgb >> 0);
185 }
186
187 writeChunk(os, ChunkType.PLTE, bytes);
188 }
189
190 private void writeChunkSCAL(final OutputStream os, final double xUPP, final double yUPP, final byte units) throws IOException {
191 final ByteArrayOutputStream baos = new ByteArrayOutputStream();
192
193
194 baos.write(units);
195
196
197 baos.write(String.valueOf(xUPP).getBytes(StandardCharsets.ISO_8859_1));
198 baos.write(0);
199
200 baos.write(String.valueOf(yUPP).getBytes(StandardCharsets.ISO_8859_1));
201
202 writeChunk(os, ChunkType.sCAL, baos.toByteArray());
203 }
204
205 private void writeChunktEXt(final OutputStream os, final AbstractPngText.Text text) throws IOException, ImagingException {
206 if (!isValidISO_8859_1(text.keyword)) {
207 throw new ImagingException("PNG tEXt chunk keyword is not ISO-8859-1: " + text.keyword);
208 }
209 if (!isValidISO_8859_1(text.text)) {
210 throw new ImagingException("PNG tEXt chunk text is not ISO-8859-1: " + text.text);
211 }
212
213 final ByteArrayOutputStream baos = new ByteArrayOutputStream();
214
215
216 baos.write(text.keyword.getBytes(StandardCharsets.ISO_8859_1));
217 baos.write(0);
218
219
220 baos.write(text.text.getBytes(StandardCharsets.ISO_8859_1));
221
222 writeChunk(os, ChunkType.tEXt, baos.toByteArray());
223 }
224
225 private void writeChunkTRNS(final OutputStream os, final Palette palette) throws IOException {
226 final byte[] bytes = Allocator.byteArray(palette.length());
227
228 for (int i = 0; i < bytes.length; i++) {
229 bytes[i] = (byte) (0xff & palette.getEntry(i) >> 24);
230 }
231
232 writeChunk(os, ChunkType.tRNS, bytes);
233 }
234
235 private void writeChunkXmpiTXt(final OutputStream os, final String xmpXml) throws IOException {
236
237 final ByteArrayOutputStream baos = new ByteArrayOutputStream();
238
239
240 baos.write(PngConstants.XMP_KEYWORD.getBytes(StandardCharsets.ISO_8859_1));
241 baos.write(0);
242
243 baos.write(1);
244 baos.write(PngConstants.COMPRESSION_DEFLATE_INFLATE);
245
246 baos.write(0);
247
248
249 baos.write(PngConstants.XMP_KEYWORD.getBytes(StandardCharsets.UTF_8));
250 baos.write(0);
251
252 baos.write(deflate(xmpXml.getBytes(StandardCharsets.UTF_8)));
253
254 writeChunk(os, ChunkType.iTXt, baos.toByteArray());
255 }
256
257 private void writeChunkzTXt(final OutputStream os, final AbstractPngText.Ztxt text) throws IOException, ImagingException {
258 if (!isValidISO_8859_1(text.keyword)) {
259 throw new ImagingException("PNG zTXt chunk keyword is not ISO-8859-1: " + text.keyword);
260 }
261 if (!isValidISO_8859_1(text.text)) {
262 throw new ImagingException("PNG zTXt chunk text is not ISO-8859-1: " + text.text);
263 }
264
265 final ByteArrayOutputStream baos = new ByteArrayOutputStream();
266
267
268 baos.write(text.keyword.getBytes(StandardCharsets.ISO_8859_1));
269 baos.write(0);
270
271
272 baos.write(PngConstants.COMPRESSION_DEFLATE_INFLATE);
273
274
275 baos.write(deflate(text.text.getBytes(StandardCharsets.ISO_8859_1)));
276
277 writeChunk(os, ChunkType.zTXt, baos.toByteArray());
278 }
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299 public void writeImage(final BufferedImage src, final OutputStream os, PngImagingParameters params, PaletteFactory paletteFactory)
300 throws ImagingException, IOException {
301 if (params == null) {
302 params = new PngImagingParameters();
303 }
304 if (paletteFactory == null) {
305 paletteFactory = new PaletteFactory();
306 }
307 final int compressionLevel = Deflater.DEFAULT_COMPRESSION;
308
309 final int width = src.getWidth();
310 final int height = src.getHeight();
311
312 final boolean hasAlpha = paletteFactory.hasTransparency(src);
313 Debug.debug("hasAlpha: " + hasAlpha);
314
315
316 boolean isGrayscale = paletteFactory.isGrayscale(src);
317 Debug.debug("isGrayscale: " + isGrayscale);
318
319 PngColorType pngColorType;
320 {
321 final boolean forceIndexedColor = params.isForceIndexedColor();
322 final boolean forceTrueColor = params.isForceTrueColor();
323
324 if (forceIndexedColor && forceTrueColor) {
325 throw new ImagingException("Params: Cannot force both indexed and true color modes");
326 }
327 if (forceIndexedColor) {
328 pngColorType = PngColorType.INDEXED_COLOR;
329 } else if (forceTrueColor) {
330 pngColorType = hasAlpha ? PngColorType.TRUE_COLOR_WITH_ALPHA : PngColorType.TRUE_COLOR;
331 isGrayscale = false;
332 } else {
333 pngColorType = PngColorType.getColorType(hasAlpha, isGrayscale);
334 }
335 Debug.debug("colorType: " + pngColorType);
336 }
337
338 final byte bitDepth = getBitDepth(pngColorType, params);
339 Debug.debug("bitDepth: " + bitDepth);
340
341 int sampleDepth;
342 if (pngColorType == PngColorType.INDEXED_COLOR) {
343 sampleDepth = 8;
344 } else {
345 sampleDepth = bitDepth;
346 }
347 Debug.debug("sampleDepth: " + sampleDepth);
348
349 {
350 PngConstants.PNG_SIGNATURE.writeTo(os);
351 }
352 {
353
354
355 final byte compressionMethod = PngConstants.COMPRESSION_TYPE_INFLATE_DEFLATE;
356 final byte filterMethod = PngConstants.FILTER_METHOD_ADAPTIVE;
357 final InterlaceMethod interlaceMethod = InterlaceMethod.NONE;
358
359 final ImageHeader imageHeader = new ImageHeader(width, height, bitDepth, pngColorType, compressionMethod, filterMethod, interlaceMethod);
360
361 writeChunkIHDR(os, imageHeader);
362 }
363
364
365
366
367
368
369
370
371 Palette palette = null;
372 if (pngColorType == PngColorType.INDEXED_COLOR) {
373
374
375 final int maxColors = 256;
376
377 if (hasAlpha) {
378 palette = paletteFactory.makeQuantizedRgbaPalette(src, hasAlpha, maxColors);
379 writeChunkPLTE(os, palette);
380 writeChunkTRNS(os, palette);
381 } else {
382 palette = paletteFactory.makeQuantizedRgbPalette(src, maxColors);
383 writeChunkPLTE(os, palette);
384 }
385 }
386
387 final Object pixelDensityObj = params.getPixelDensity();
388 if (pixelDensityObj != null) {
389 final PixelDensity pixelDensity = (PixelDensity) pixelDensityObj;
390 if (pixelDensity.isUnitless()) {
391 writeChunkPHYS(os, (int) Math.round(pixelDensity.getRawHorizontalDensity()), (int) Math.round(pixelDensity.getRawVerticalDensity()), (byte) 0);
392 } else {
393 writeChunkPHYS(os, (int) Math.round(pixelDensity.horizontalDensityMetres()), (int) Math.round(pixelDensity.verticalDensityMetres()), (byte) 1);
394 }
395 }
396
397 final PhysicalScale physicalScale = params.getPhysicalScale();
398 if (physicalScale != null) {
399 writeChunkSCAL(os, physicalScale.getHorizontalUnitsPerPixel(), physicalScale.getVerticalUnitsPerPixel(),
400 physicalScale.isInMeters() ? (byte) 1 : (byte) 2);
401 }
402
403 final String xmpXml = params.getXmpXml();
404 if (xmpXml != null) {
405 writeChunkXmpiTXt(os, xmpXml);
406 }
407
408 final List<? extends AbstractPngText> outputTexts = params.getTextChunks();
409 if (outputTexts != null) {
410 for (final AbstractPngText text : outputTexts) {
411 if (text instanceof AbstractPngText.Text) {
412 writeChunktEXt(os, (AbstractPngText.Text) text);
413 } else if (text instanceof AbstractPngText.Ztxt) {
414 writeChunkzTXt(os, (AbstractPngText.Ztxt) text);
415 } else if (text instanceof AbstractPngText.Itxt) {
416 writeChunkiTXt(os, (AbstractPngText.Itxt) text);
417 } else {
418 throw new ImagingException("Unknown text to embed in PNG: " + text);
419 }
420 }
421 }
422
423 {
424
425
426
427
428
429
430
431
432 final boolean usePredictor = params.isPredictorEnabled() && !isGrayscale && palette == null;
433
434 byte[] uncompressed;
435 if (!usePredictor) {
436 final ByteArrayOutputStream baos = new ByteArrayOutputStream();
437
438 final boolean useAlpha = pngColorType == PngColorType.GREYSCALE_WITH_ALPHA || pngColorType == PngColorType.TRUE_COLOR_WITH_ALPHA;
439
440 final int[] row = Allocator.intArray(width);
441 for (int y = 0; y < height; y++) {
442
443 src.getRGB(0, y, width, 1, row, 0, width);
444
445 baos.write(FilterType.NONE.ordinal());
446 for (int x = 0; x < width; x++) {
447 final int argb = row[x];
448
449 if (palette != null) {
450 final int index = palette.getPaletteIndex(argb);
451 baos.write(0xff & index);
452 } else {
453 final int alpha = 0xff & argb >> 24;
454 final int red = 0xff & argb >> 16;
455 final int green = 0xff & argb >> 8;
456 final int blue = 0xff & argb >> 0;
457
458 if (isGrayscale) {
459 final int gray = (red + green + blue) / 3;
460
461
462
463
464
465
466
467
468
469
470
471
472 baos.write(gray);
473 } else {
474 baos.write(red);
475 baos.write(green);
476 baos.write(blue);
477 }
478 if (useAlpha) {
479 baos.write(alpha);
480 }
481 }
482 }
483 }
484 uncompressed = baos.toByteArray();
485 } else {
486 final ByteArrayOutputStream baos = new ByteArrayOutputStream();
487
488 final boolean useAlpha = pngColorType == PngColorType.GREYSCALE_WITH_ALPHA || pngColorType == PngColorType.TRUE_COLOR_WITH_ALPHA;
489
490 final int[] row = Allocator.intArray(width);
491 for (int y = 0; y < height; y++) {
492
493 src.getRGB(0, y, width, 1, row, 0, width);
494
495 int priorA = 0;
496 int priorR = 0;
497 int priorG = 0;
498 int priorB = 0;
499 baos.write(FilterType.SUB.ordinal());
500 for (int x = 0; x < width; x++) {
501 final int argb = row[x];
502 final int alpha = 0xff & argb >> 24;
503 final int red = 0xff & argb >> 16;
504 final int green = 0xff & argb >> 8;
505 final int blue = 0xff & argb;
506
507 baos.write(red - priorR);
508 baos.write(green - priorG);
509 baos.write(blue - priorB);
510 priorR = red;
511 priorG = green;
512 priorB = blue;
513
514 if (useAlpha) {
515 baos.write(alpha - priorA);
516 priorA = alpha;
517 }
518 }
519 }
520 uncompressed = baos.toByteArray();
521 }
522
523
524
525 final ByteArrayOutputStream baos = new ByteArrayOutputStream();
526 final int chunkSize = 256 * 1024;
527 final Deflater deflater = new Deflater(compressionLevel);
528 final DeflaterOutputStream dos = new DeflaterOutputStream(baos, deflater, chunkSize);
529
530 for (int index = 0; index < uncompressed.length; index += chunkSize) {
531 final int end = Math.min(uncompressed.length, index + chunkSize);
532 final int length = end - index;
533
534 dos.write(uncompressed, index, length);
535 dos.flush();
536 baos.flush();
537
538 final byte[] compressed = baos.toByteArray();
539 baos.reset();
540 if (compressed.length > 0) {
541
542 writeChunkIDAT(os, compressed);
543 }
544
545 }
546 {
547 dos.finish();
548 final byte[] compressed = baos.toByteArray();
549 if (compressed.length > 0) {
550
551 writeChunkIDAT(os, compressed);
552 }
553 }
554 }
555
556 {
557
558
559 writeChunkIEND(os);
560 }
561
562
563
564
565
566
567
568
569
570 os.close();
571 }
572
573
574
575 private void writeInt(final OutputStream os, final int value) throws IOException {
576 os.write(0xff & value >> 24);
577 os.write(0xff & value >> 16);
578 os.write(0xff & value >> 8);
579 os.write(0xff & value >> 0);
580 }
581 }