001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: */
017:
018: /* $Id: PNGImageEncoder.java 447277 2006-09-18 06:19:34Z jeremias $ */
019:
020: package org.apache.xmlgraphics.image.codec.png;
021:
022: import org.apache.xmlgraphics.image.codec.util.ImageEncoderImpl;
023:
024: import java.awt.Rectangle;
025: import java.awt.image.ColorModel;
026: import java.awt.image.IndexColorModel;
027: import java.awt.image.Raster;
028: import java.awt.image.RenderedImage;
029: import java.awt.image.SampleModel;
030: import java.io.ByteArrayOutputStream;
031: import java.io.DataOutput;
032: import java.io.DataOutputStream;
033: import java.io.FilterOutputStream;
034: import java.io.IOException;
035: import java.io.OutputStream;
036: import java.util.Calendar;
037: import java.util.Date;
038: import java.util.GregorianCalendar;
039: import java.util.TimeZone;
040: import java.util.zip.Deflater;
041: import java.util.zip.DeflaterOutputStream;
042:
043: class CRC {
044:
045: private static int[] crcTable = new int[256];
046:
047: static {
048: // Initialize CRC table
049: for (int n = 0; n < 256; n++) {
050: int c = n;
051: for (int k = 0; k < 8; k++) {
052: if ((c & 1) == 1) {
053: c = 0xedb88320 ^ (c >>> 1);
054: } else {
055: c >>>= 1;
056: }
057:
058: crcTable[n] = c;
059: }
060: }
061: }
062:
063: public static int updateCRC(int crc, byte[] data, int off, int len) {
064: int c = crc;
065:
066: for (int n = 0; n < len; n++) {
067: c = crcTable[(c ^ data[off + n]) & 0xff] ^ (c >>> 8);
068: }
069:
070: return c;
071: }
072: }
073:
074: class ChunkStream extends OutputStream implements DataOutput {
075:
076: private String type;
077: private ByteArrayOutputStream baos;
078: private DataOutputStream dos;
079:
080: public ChunkStream(String type) throws IOException {
081: this .type = type;
082:
083: this .baos = new ByteArrayOutputStream();
084: this .dos = new DataOutputStream(baos);
085: }
086:
087: public void write(byte[] b) throws IOException {
088: dos.write(b);
089: }
090:
091: public void write(byte[] b, int off, int len) throws IOException {
092: dos.write(b, off, len);
093: }
094:
095: public void write(int b) throws IOException {
096: dos.write(b);
097: }
098:
099: public void writeBoolean(boolean v) throws IOException {
100: dos.writeBoolean(v);
101: }
102:
103: public void writeByte(int v) throws IOException {
104: dos.writeByte(v);
105: }
106:
107: public void writeBytes(String s) throws IOException {
108: dos.writeBytes(s);
109: }
110:
111: public void writeChar(int v) throws IOException {
112: dos.writeChar(v);
113: }
114:
115: public void writeChars(String s) throws IOException {
116: dos.writeChars(s);
117: }
118:
119: public void writeDouble(double v) throws IOException {
120: dos.writeDouble(v);
121: }
122:
123: public void writeFloat(float v) throws IOException {
124: dos.writeFloat(v);
125: }
126:
127: public void writeInt(int v) throws IOException {
128: dos.writeInt(v);
129: }
130:
131: public void writeLong(long v) throws IOException {
132: dos.writeLong(v);
133: }
134:
135: public void writeShort(int v) throws IOException {
136: dos.writeShort(v);
137: }
138:
139: public void writeUTF(String str) throws IOException {
140: dos.writeUTF(str);
141: }
142:
143: public void writeToStream(DataOutputStream output)
144: throws IOException {
145: byte[] typeSignature = new byte[4];
146: typeSignature[0] = (byte) type.charAt(0);
147: typeSignature[1] = (byte) type.charAt(1);
148: typeSignature[2] = (byte) type.charAt(2);
149: typeSignature[3] = (byte) type.charAt(3);
150:
151: dos.flush();
152: baos.flush();
153:
154: byte[] data = baos.toByteArray();
155: int len = data.length;
156:
157: output.writeInt(len);
158: output.write(typeSignature);
159: output.write(data, 0, len);
160:
161: int crc = 0xffffffff;
162: crc = CRC.updateCRC(crc, typeSignature, 0, 4);
163: crc = CRC.updateCRC(crc, data, 0, len);
164: output.writeInt(crc ^ 0xffffffff);
165: }
166: }
167:
168: class IDATOutputStream extends FilterOutputStream {
169:
170: private static final byte[] typeSignature = { (byte) 'I',
171: (byte) 'D', (byte) 'A', (byte) 'T' };
172:
173: private int bytesWritten = 0;
174: private int segmentLength;
175: byte[] buffer;
176:
177: public IDATOutputStream(OutputStream output, int segmentLength) {
178: super (output);
179: this .segmentLength = segmentLength;
180: this .buffer = new byte[segmentLength];
181: }
182:
183: public void close() throws IOException {
184: flush();
185: }
186:
187: private void writeInt(int x) throws IOException {
188: out.write(x >> 24);
189: out.write((x >> 16) & 0xff);
190: out.write((x >> 8) & 0xff);
191: out.write(x & 0xff);
192: }
193:
194: public void flush() throws IOException {
195: // Length
196: writeInt(bytesWritten);
197: // 'IDAT' signature
198: out.write(typeSignature);
199: // Data
200: out.write(buffer, 0, bytesWritten);
201:
202: int crc = 0xffffffff;
203: crc = CRC.updateCRC(crc, typeSignature, 0, 4);
204: crc = CRC.updateCRC(crc, buffer, 0, bytesWritten);
205:
206: // CRC
207: writeInt(crc ^ 0xffffffff);
208:
209: // Reset buffer
210: bytesWritten = 0;
211: }
212:
213: public void write(byte[] b) throws IOException {
214: this .write(b, 0, b.length);
215: }
216:
217: public void write(byte[] b, int off, int len) throws IOException {
218: while (len > 0) {
219: int bytes = Math.min(segmentLength - bytesWritten, len);
220: System.arraycopy(b, off, buffer, bytesWritten, bytes);
221: off += bytes;
222: len -= bytes;
223: bytesWritten += bytes;
224:
225: if (bytesWritten == segmentLength) {
226: flush();
227: }
228: }
229: }
230:
231: public void write(int b) throws IOException {
232: buffer[bytesWritten++] = (byte) b;
233: if (bytesWritten == segmentLength) {
234: flush();
235: }
236: }
237: }
238:
239: /**
240: * An ImageEncoder for the PNG file format.
241: *
242: * @since EA4
243: */
244: public class PNGImageEncoder extends ImageEncoderImpl {
245:
246: private static final int PNG_COLOR_GRAY = 0;
247: private static final int PNG_COLOR_RGB = 2;
248: private static final int PNG_COLOR_PALETTE = 3;
249: private static final int PNG_COLOR_GRAY_ALPHA = 4;
250: private static final int PNG_COLOR_RGB_ALPHA = 6;
251:
252: private static final byte[] magic = { (byte) 137, (byte) 80,
253: (byte) 78, (byte) 71, (byte) 13, (byte) 10, (byte) 26,
254: (byte) 10 };
255:
256: private PNGEncodeParam param;
257:
258: private RenderedImage image;
259: private int width;
260: private int height;
261: private int bitDepth;
262: private int bitShift;
263: private int numBands;
264: private int colorType;
265:
266: private int bpp; // bytes per pixel, rounded up
267:
268: private boolean skipAlpha = false;
269: private boolean compressGray = false;
270:
271: private boolean interlace;
272:
273: private byte[] redPalette = null;
274: private byte[] greenPalette = null;
275: private byte[] bluePalette = null;
276: private byte[] alphaPalette = null;
277:
278: private DataOutputStream dataOutput;
279:
280: public PNGImageEncoder(OutputStream output, PNGEncodeParam param) {
281: super (output, param);
282:
283: if (param != null) {
284: this .param = param;
285: }
286: this .dataOutput = new DataOutputStream(output);
287: }
288:
289: private void writeMagic() throws IOException {
290: dataOutput.write(magic);
291: }
292:
293: private void writeIHDR() throws IOException {
294: ChunkStream cs = new ChunkStream("IHDR");
295: cs.writeInt(width);
296: cs.writeInt(height);
297: cs.writeByte((byte) bitDepth);
298: cs.writeByte((byte) colorType);
299: cs.writeByte((byte) 0);
300: cs.writeByte((byte) 0);
301: cs.writeByte(interlace ? (byte) 1 : (byte) 0);
302:
303: cs.writeToStream(dataOutput);
304: }
305:
306: private byte[] prevRow = null;
307: private byte[] currRow = null;
308:
309: private byte[][] filteredRows = null;
310:
311: private static int clamp(int val, int maxValue) {
312: return (val > maxValue) ? maxValue : val;
313: }
314:
315: private void encodePass(OutputStream os, Raster ras, int xOffset,
316: int yOffset, int xSkip, int ySkip) throws IOException {
317: int minX = ras.getMinX();
318: int minY = ras.getMinY();
319: int width = ras.getWidth();
320: int height = ras.getHeight();
321:
322: xOffset *= numBands;
323: xSkip *= numBands;
324:
325: int samplesPerByte = 8 / bitDepth;
326:
327: int numSamples = width * numBands;
328: int[] samples = new int[numSamples];
329:
330: int pixels = (numSamples - xOffset + xSkip - 1) / xSkip;
331: int bytesPerRow = pixels * numBands;
332: if (bitDepth < 8) {
333: bytesPerRow = (bytesPerRow + samplesPerByte - 1)
334: / samplesPerByte;
335: } else if (bitDepth == 16) {
336: bytesPerRow *= 2;
337: }
338:
339: if (bytesPerRow == 0) {
340: return;
341: }
342:
343: currRow = new byte[bytesPerRow + bpp];
344: prevRow = new byte[bytesPerRow + bpp];
345:
346: filteredRows = new byte[5][bytesPerRow + bpp];
347:
348: int maxValue = (1 << bitDepth) - 1;
349:
350: for (int row = minY + yOffset; row < minY + height; row += ySkip) {
351: ras.getPixels(minX, row, width, 1, samples);
352:
353: if (compressGray) {
354: int shift = 8 - bitDepth;
355: for (int i = 0; i < width; i++) {
356: samples[i] >>= shift;
357: }
358: }
359:
360: int count = bpp; // leave first 'bpp' bytes zero
361: int pos = 0;
362: int tmp = 0;
363:
364: switch (bitDepth) {
365: case 1:
366: case 2:
367: case 4:
368: // Image can only have a single band
369:
370: int mask = samplesPerByte - 1;
371: for (int s = xOffset; s < numSamples; s += xSkip) {
372: int val = clamp(samples[s] >> bitShift, maxValue);
373: tmp = (tmp << bitDepth) | val;
374:
375: if (pos++ == mask) {
376: currRow[count++] = (byte) tmp;
377: tmp = 0;
378: pos = 0;
379: }
380: }
381:
382: // Left shift the last byte
383: if (pos != 0) {
384: tmp <<= (samplesPerByte - pos) * bitDepth;
385: currRow[count++] = (byte) tmp;
386: }
387: break;
388:
389: case 8:
390: for (int s = xOffset; s < numSamples; s += xSkip) {
391: for (int b = 0; b < numBands; b++) {
392: currRow[count++] = (byte) clamp(
393: samples[s + b] >> bitShift, maxValue);
394: }
395: }
396: break;
397:
398: case 16:
399: for (int s = xOffset; s < numSamples; s += xSkip) {
400: for (int b = 0; b < numBands; b++) {
401: int val = clamp(samples[s + b] >> bitShift,
402: maxValue);
403: currRow[count++] = (byte) (val >> 8);
404: currRow[count++] = (byte) (val & 0xff);
405: }
406: }
407: break;
408: }
409:
410: // Perform filtering
411: int filterType = param.filterRow(currRow, prevRow,
412: filteredRows, bytesPerRow, bpp);
413:
414: os.write(filterType);
415: os.write(filteredRows[filterType], bpp, bytesPerRow);
416:
417: // Swap current and previous rows
418: byte[] swap = currRow;
419: currRow = prevRow;
420: prevRow = swap;
421: }
422: }
423:
424: private void writeIDAT() throws IOException {
425: IDATOutputStream ios = new IDATOutputStream(dataOutput, 8192);
426: DeflaterOutputStream dos = new DeflaterOutputStream(ios,
427: new Deflater(9));
428:
429: // Future work - don't convert entire image to a Raster It
430: // might seem that you could just call image.getData() but
431: // 'BufferedImage.subImage' doesn't appear to set the Width
432: // and height properly of the Child Raster, so the Raster
433: // you get back here appears larger than it should.
434: // This solves that problem by bounding the raster to the
435: // image's bounds...
436: Raster ras = image.getData(new Rectangle(image.getMinX(), image
437: .getMinY(), image.getWidth(), image.getHeight()));
438: // System.out.println("Image: [" +
439: // image.getMinY() + ", " +
440: // image.getMinX() + ", " +
441: // image.getWidth() + ", " +
442: // image.getHeight() + "]");
443: // System.out.println("Ras: [" +
444: // ras.getMinX() + ", " +
445: // ras.getMinY() + ", " +
446: // ras.getWidth() + ", " +
447: // ras.getHeight() + "]");
448:
449: if (skipAlpha) {
450: int numBands = ras.getNumBands() - 1;
451: int[] bandList = new int[numBands];
452: for (int i = 0; i < numBands; i++) {
453: bandList[i] = i;
454: }
455: ras = ras.createChild(0, 0, ras.getWidth(),
456: ras.getHeight(), 0, 0, bandList);
457: }
458:
459: if (interlace) {
460: // Interlacing pass 1
461: encodePass(dos, ras, 0, 0, 8, 8);
462: // Interlacing pass 2
463: encodePass(dos, ras, 4, 0, 8, 8);
464: // Interlacing pass 3
465: encodePass(dos, ras, 0, 4, 4, 8);
466: // Interlacing pass 4
467: encodePass(dos, ras, 2, 0, 4, 4);
468: // Interlacing pass 5
469: encodePass(dos, ras, 0, 2, 2, 4);
470: // Interlacing pass 6
471: encodePass(dos, ras, 1, 0, 2, 2);
472: // Interlacing pass 7
473: encodePass(dos, ras, 0, 1, 1, 2);
474: } else {
475: encodePass(dos, ras, 0, 0, 1, 1);
476: }
477:
478: dos.finish();
479: ios.flush();
480: }
481:
482: private void writeIEND() throws IOException {
483: ChunkStream cs = new ChunkStream("IEND");
484: cs.writeToStream(dataOutput);
485: }
486:
487: private static final float[] srgbChroma = { 0.31270F, 0.329F,
488: 0.64F, 0.33F, 0.3F, 0.6F, 0.15F, 0.06F };
489:
490: private void writeCHRM() throws IOException {
491: if (param.isChromaticitySet() || param.isSRGBIntentSet()) {
492: ChunkStream cs = new ChunkStream("cHRM");
493:
494: float[] chroma;
495: if (!param.isSRGBIntentSet()) {
496: chroma = param.getChromaticity();
497: } else {
498: chroma = srgbChroma; // SRGB chromaticities
499: }
500:
501: for (int i = 0; i < 8; i++) {
502: cs.writeInt((int) (chroma[i] * 100000));
503: }
504: cs.writeToStream(dataOutput);
505: }
506: }
507:
508: private void writeGAMA() throws IOException {
509: if (param.isGammaSet() || param.isSRGBIntentSet()) {
510: ChunkStream cs = new ChunkStream("gAMA");
511:
512: float gamma;
513: if (!param.isSRGBIntentSet()) {
514: gamma = param.getGamma();
515: } else {
516: gamma = 1.0F / 2.2F; // SRGB gamma
517: }
518: // TD should include the .5 but causes regard to say
519: // everything is different.
520: cs.writeInt((int) (gamma * 100000/*+0.5*/));
521: cs.writeToStream(dataOutput);
522: }
523: }
524:
525: private void writeICCP() throws IOException {
526: if (param.isICCProfileDataSet()) {
527: ChunkStream cs = new ChunkStream("iCCP");
528: byte[] ICCProfileData = param.getICCProfileData();
529: cs.write(ICCProfileData);
530: cs.writeToStream(dataOutput);
531: }
532: }
533:
534: private void writeSBIT() throws IOException {
535: if (param.isSignificantBitsSet()) {
536: ChunkStream cs = new ChunkStream("sBIT");
537: int[] significantBits = param.getSignificantBits();
538: int len = significantBits.length;
539: for (int i = 0; i < len; i++) {
540: cs.writeByte(significantBits[i]);
541: }
542: cs.writeToStream(dataOutput);
543: }
544: }
545:
546: private void writeSRGB() throws IOException {
547: if (param.isSRGBIntentSet()) {
548: ChunkStream cs = new ChunkStream("sRGB");
549:
550: int intent = param.getSRGBIntent();
551: cs.write(intent);
552: cs.writeToStream(dataOutput);
553: }
554: }
555:
556: private void writePLTE() throws IOException {
557: if (redPalette == null) {
558: return;
559: }
560:
561: ChunkStream cs = new ChunkStream("PLTE");
562: for (int i = 0; i < redPalette.length; i++) {
563: cs.writeByte(redPalette[i]);
564: cs.writeByte(greenPalette[i]);
565: cs.writeByte(bluePalette[i]);
566: }
567:
568: cs.writeToStream(dataOutput);
569: }
570:
571: private void writeBKGD() throws IOException {
572: if (param.isBackgroundSet()) {
573: ChunkStream cs = new ChunkStream("bKGD");
574:
575: switch (colorType) {
576: case PNG_COLOR_GRAY:
577: case PNG_COLOR_GRAY_ALPHA:
578: int gray = ((PNGEncodeParam.Gray) param)
579: .getBackgroundGray();
580: cs.writeShort(gray);
581: break;
582:
583: case PNG_COLOR_PALETTE:
584: int index = ((PNGEncodeParam.Palette) param)
585: .getBackgroundPaletteIndex();
586: cs.writeByte(index);
587: break;
588:
589: case PNG_COLOR_RGB:
590: case PNG_COLOR_RGB_ALPHA:
591: int[] rgb = ((PNGEncodeParam.RGB) param)
592: .getBackgroundRGB();
593: cs.writeShort(rgb[0]);
594: cs.writeShort(rgb[1]);
595: cs.writeShort(rgb[2]);
596: break;
597: }
598:
599: cs.writeToStream(dataOutput);
600: }
601: }
602:
603: private void writeHIST() throws IOException {
604: if (param.isPaletteHistogramSet()) {
605: ChunkStream cs = new ChunkStream("hIST");
606:
607: int[] hist = param.getPaletteHistogram();
608: for (int i = 0; i < hist.length; i++) {
609: cs.writeShort(hist[i]);
610: }
611:
612: cs.writeToStream(dataOutput);
613: }
614: }
615:
616: private void writeTRNS() throws IOException {
617: if (param.isTransparencySet()
618: && (colorType != PNG_COLOR_GRAY_ALPHA)
619: && (colorType != PNG_COLOR_RGB_ALPHA)) {
620: ChunkStream cs = new ChunkStream("tRNS");
621:
622: if (param instanceof PNGEncodeParam.Palette) {
623: byte[] t = ((PNGEncodeParam.Palette) param)
624: .getPaletteTransparency();
625: for (int i = 0; i < t.length; i++) {
626: cs.writeByte(t[i]);
627: }
628: } else if (param instanceof PNGEncodeParam.Gray) {
629: int t = ((PNGEncodeParam.Gray) param)
630: .getTransparentGray();
631: cs.writeShort(t);
632: } else if (param instanceof PNGEncodeParam.RGB) {
633: int[] t = ((PNGEncodeParam.RGB) param)
634: .getTransparentRGB();
635: cs.writeShort(t[0]);
636: cs.writeShort(t[1]);
637: cs.writeShort(t[2]);
638: }
639:
640: cs.writeToStream(dataOutput);
641: } else if (colorType == PNG_COLOR_PALETTE) {
642: int lastEntry = Math.min(255, alphaPalette.length - 1);
643: int nonOpaque;
644: for (nonOpaque = lastEntry; nonOpaque >= 0; nonOpaque--) {
645: if (alphaPalette[nonOpaque] != (byte) 255) {
646: break;
647: }
648: }
649:
650: if (nonOpaque >= 0) {
651: ChunkStream cs = new ChunkStream("tRNS");
652: for (int i = 0; i <= nonOpaque; i++) {
653: cs.writeByte(alphaPalette[i]);
654: }
655: cs.writeToStream(dataOutput);
656: }
657: }
658: }
659:
660: private void writePHYS() throws IOException {
661: if (param.isPhysicalDimensionSet()) {
662: ChunkStream cs = new ChunkStream("pHYs");
663:
664: int[] dims = param.getPhysicalDimension();
665: cs.writeInt(dims[0]);
666: cs.writeInt(dims[1]);
667: cs.writeByte((byte) dims[2]);
668:
669: cs.writeToStream(dataOutput);
670: }
671: }
672:
673: private void writeSPLT() throws IOException {
674: if (param.isSuggestedPaletteSet()) {
675: ChunkStream cs = new ChunkStream("sPLT");
676:
677: System.out.println("sPLT not supported yet.");
678:
679: cs.writeToStream(dataOutput);
680: }
681: }
682:
683: private void writeTIME() throws IOException {
684: if (param.isModificationTimeSet()) {
685: ChunkStream cs = new ChunkStream("tIME");
686:
687: Date date = param.getModificationTime();
688: TimeZone gmt = TimeZone.getTimeZone("GMT");
689:
690: GregorianCalendar cal = new GregorianCalendar(gmt);
691: cal.setTime(date);
692:
693: int year = cal.get(Calendar.YEAR);
694: int month = cal.get(Calendar.MONTH);
695: int day = cal.get(Calendar.DAY_OF_MONTH);
696: int hour = cal.get(Calendar.HOUR_OF_DAY);
697: int minute = cal.get(Calendar.MINUTE);
698: int second = cal.get(Calendar.SECOND);
699:
700: cs.writeShort(year);
701: cs.writeByte(month + 1);
702: cs.writeByte(day);
703: cs.writeByte(hour);
704: cs.writeByte(minute);
705: cs.writeByte(second);
706:
707: cs.writeToStream(dataOutput);
708: }
709: }
710:
711: private void writeTEXT() throws IOException {
712: if (param.isTextSet()) {
713: String[] text = param.getText();
714:
715: for (int i = 0; i < text.length / 2; i++) {
716: byte[] keyword = text[2 * i].getBytes();
717: byte[] value = text[2 * i + 1].getBytes();
718:
719: ChunkStream cs = new ChunkStream("tEXt");
720:
721: cs.write(keyword, 0, Math.min(keyword.length, 79));
722: cs.write(0);
723: cs.write(value);
724:
725: cs.writeToStream(dataOutput);
726: }
727: }
728: }
729:
730: private void writeZTXT() throws IOException {
731: if (param.isCompressedTextSet()) {
732: String[] text = param.getCompressedText();
733:
734: for (int i = 0; i < text.length / 2; i++) {
735: byte[] keyword = text[2 * i].getBytes();
736: byte[] value = text[2 * i + 1].getBytes();
737:
738: ChunkStream cs = new ChunkStream("zTXt");
739:
740: cs.write(keyword, 0, Math.min(keyword.length, 79));
741: cs.write(0);
742: cs.write(0);
743:
744: DeflaterOutputStream dos = new DeflaterOutputStream(cs);
745: dos.write(value);
746: dos.finish();
747:
748: cs.writeToStream(dataOutput);
749: }
750: }
751: }
752:
753: private void writePrivateChunks() throws IOException {
754: int numChunks = param.getNumPrivateChunks();
755: for (int i = 0; i < numChunks; i++) {
756: String type = param.getPrivateChunkType(i);
757: byte[] data = param.getPrivateChunkData(i);
758:
759: ChunkStream cs = new ChunkStream(type);
760: cs.write(data);
761: cs.writeToStream(dataOutput);
762: }
763: }
764:
765: /**
766: * Analyzes a set of palettes and determines if it can be expressed
767: * as a standard set of gray values, with zero or one values being
768: * fully transparent and the rest being fully opaque. If it
769: * is possible to express the data thusly, the method returns
770: * a suitable instance of PNGEncodeParam.Gray; otherwise it
771: * returns null.
772: */
773: private PNGEncodeParam.Gray createGrayParam(byte[] redPalette,
774: byte[] greenPalette, byte[] bluePalette, byte[] alphaPalette) {
775: PNGEncodeParam.Gray param = new PNGEncodeParam.Gray();
776: int numTransparent = 0;
777:
778: int grayFactor = 255 / ((1 << bitDepth) - 1);
779: int entries = 1 << bitDepth;
780: for (int i = 0; i < entries; i++) {
781: byte red = redPalette[i];
782: if ((red != i * grayFactor) || (red != greenPalette[i])
783: || (red != bluePalette[i])) {
784: return null;
785: }
786:
787: // All alphas must be 255 except at most 1 can be 0
788: byte alpha = alphaPalette[i];
789: if (alpha == (byte) 0) {
790: param.setTransparentGray(i);
791:
792: ++numTransparent;
793: if (numTransparent > 1) {
794: return null;
795: }
796: } else if (alpha != (byte) 255) {
797: return null;
798: }
799: }
800:
801: return param;
802: }
803:
804: /**
805: * This method encodes a <code>RenderedImage</code> into PNG.
806: * The stream into which the PNG is dumped is not closed at
807: * the end of the operation, this should be done if needed
808: * by the caller of this method.
809: */
810: public void encode(RenderedImage im) throws IOException {
811: this .image = im;
812: this .width = image.getWidth();
813: this .height = image.getHeight();
814:
815: SampleModel sampleModel = image.getSampleModel();
816:
817: int[] sampleSize = sampleModel.getSampleSize();
818:
819: // Set bitDepth to a sentinel value
820: this .bitDepth = -1;
821: this .bitShift = 0;
822:
823: // Allow user to override the bit depth of gray images
824: if (param instanceof PNGEncodeParam.Gray) {
825: PNGEncodeParam.Gray paramg = (PNGEncodeParam.Gray) param;
826: if (paramg.isBitDepthSet()) {
827: this .bitDepth = paramg.getBitDepth();
828: }
829:
830: if (paramg.isBitShiftSet()) {
831: this .bitShift = paramg.getBitShift();
832: }
833: }
834:
835: // Get bit depth from image if not set in param
836: if (this .bitDepth == -1) {
837: // Get bit depth from channel 0 of the image
838:
839: this .bitDepth = sampleSize[0];
840: // Ensure all channels have the same bit depth
841: for (int i = 1; i < sampleSize.length; i++) {
842: if (sampleSize[i] != bitDepth) {
843: throw new RuntimeException();
844: }
845: }
846:
847: // Round bit depth up to a power of 2
848: if (bitDepth > 2 && bitDepth < 4) {
849: bitDepth = 4;
850: } else if (bitDepth > 4 && bitDepth < 8) {
851: bitDepth = 8;
852: } else if (bitDepth > 8 && bitDepth < 16) {
853: bitDepth = 16;
854: } else if (bitDepth > 16) {
855: throw new RuntimeException();
856: }
857: }
858:
859: this .numBands = sampleModel.getNumBands();
860: this .bpp = numBands * ((bitDepth == 16) ? 2 : 1);
861:
862: ColorModel colorModel = image.getColorModel();
863: if (colorModel instanceof IndexColorModel) {
864: if (bitDepth < 1 || bitDepth > 8) {
865: throw new RuntimeException();
866: }
867: if (sampleModel.getNumBands() != 1) {
868: throw new RuntimeException();
869: }
870:
871: IndexColorModel icm = (IndexColorModel) colorModel;
872: int size = icm.getMapSize();
873:
874: redPalette = new byte[size];
875: greenPalette = new byte[size];
876: bluePalette = new byte[size];
877: alphaPalette = new byte[size];
878:
879: icm.getReds(redPalette);
880: icm.getGreens(greenPalette);
881: icm.getBlues(bluePalette);
882: icm.getAlphas(alphaPalette);
883:
884: this .bpp = 1;
885:
886: if (param == null) {
887: param = createGrayParam(redPalette, greenPalette,
888: bluePalette, alphaPalette);
889: }
890:
891: // If param is still null, it can't be expressed as gray
892: if (param == null) {
893: param = new PNGEncodeParam.Palette();
894: }
895:
896: if (param instanceof PNGEncodeParam.Palette) {
897: // If palette not set in param, create one from the ColorModel.
898: PNGEncodeParam.Palette parami = (PNGEncodeParam.Palette) param;
899: if (parami.isPaletteSet()) {
900: int[] palette = parami.getPalette();
901: size = palette.length / 3;
902:
903: int index = 0;
904: for (int i = 0; i < size; i++) {
905: redPalette[i] = (byte) palette[index++];
906: greenPalette[i] = (byte) palette[index++];
907: bluePalette[i] = (byte) palette[index++];
908: alphaPalette[i] = (byte) 255;
909: }
910: }
911: this .colorType = PNG_COLOR_PALETTE;
912: } else if (param instanceof PNGEncodeParam.Gray) {
913: redPalette = greenPalette = bluePalette = alphaPalette = null;
914: this .colorType = PNG_COLOR_GRAY;
915: } else {
916: throw new RuntimeException();
917: }
918: } else if (numBands == 1) {
919: if (param == null) {
920: param = new PNGEncodeParam.Gray();
921: }
922: this .colorType = PNG_COLOR_GRAY;
923: } else if (numBands == 2) {
924: if (param == null) {
925: param = new PNGEncodeParam.Gray();
926: }
927:
928: if (param.isTransparencySet()) {
929: skipAlpha = true;
930: numBands = 1;
931: if ((sampleSize[0] == 8) && (bitDepth < 8)) {
932: compressGray = true;
933: }
934: bpp = (bitDepth == 16) ? 2 : 1;
935: this .colorType = PNG_COLOR_GRAY;
936: } else {
937: if (this .bitDepth < 8) {
938: this .bitDepth = 8;
939: }
940: this .colorType = PNG_COLOR_GRAY_ALPHA;
941: }
942: } else if (numBands == 3) {
943: if (param == null) {
944: param = new PNGEncodeParam.RGB();
945: }
946: this .colorType = PNG_COLOR_RGB;
947: } else if (numBands == 4) {
948: if (param == null) {
949: param = new PNGEncodeParam.RGB();
950: }
951: if (param.isTransparencySet()) {
952: skipAlpha = true;
953: numBands = 3;
954: bpp = (bitDepth == 16) ? 6 : 3;
955: this.colorType = PNG_COLOR_RGB;
956: } else {
957: this.colorType = PNG_COLOR_RGB_ALPHA;
958: }
959: }
960:
961: interlace = param.getInterlacing();
962:
963: writeMagic();
964:
965: writeIHDR();
966:
967: writeCHRM();
968: writeGAMA();
969: writeICCP();
970: writeSBIT();
971: writeSRGB();
972:
973: writePLTE();
974:
975: writeHIST();
976: writeTRNS();
977: writeBKGD();
978:
979: writePHYS();
980: writeSPLT();
981: writeTIME();
982: writeTEXT();
983: writeZTXT();
984:
985: writePrivateChunks();
986:
987: writeIDAT();
988:
989: writeIEND();
990:
991: dataOutput.flush();
992: }
993: }
|