View Javadoc
1   /*
2    *  Licensed under the Apache License, Version 2.0 (the "License");
3    *  you may not use this file except in compliance with the License.
4    *  You may obtain a copy of the License at
5    *
6    *       http://www.apache.org/licenses/LICENSE-2.0
7    *
8    *  Unless required by applicable law or agreed to in writing, software
9    *  distributed under the License is distributed on an "AS IS" BASIS,
10   *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11   *  See the License for the specific language governing permissions and
12   *  limitations under the License.
13   *  under the License.
14   */
15  
16  package org.apache.commons.imaging.formats.pcx;
17  
18  import java.awt.image.BufferedImage;
19  import java.io.IOException;
20  import java.io.OutputStream;
21  import java.util.Arrays;
22  
23  import org.apache.commons.imaging.PixelDensity;
24  import org.apache.commons.imaging.common.AbstractBinaryOutputStream;
25  import org.apache.commons.imaging.common.Allocator;
26  import org.apache.commons.imaging.palette.PaletteFactory;
27  import org.apache.commons.imaging.palette.SimplePalette;
28  
29  final class PcxWriter {
30      private final int encoding;
31      private final int bitDepthWanted;
32      private final int planesWanted;
33      private final PixelDensity pixelDensity;
34      private final RleWriter rleWriter;
35  
36      PcxWriter(PcxImagingParameters params) {
37          // uncompressed PCX files are not even documented in ZSoft's spec,
38          // let alone supported by most image viewers
39          if (params == null) {
40              params = new PcxImagingParameters();
41          }
42          encoding = params.getCompression() == PcxConstants.PCX_COMPRESSION_UNCOMPRESSED ? PcxImageParser.PcxHeader.ENCODING_UNCOMPRESSED
43                  : PcxImageParser.PcxHeader.ENCODING_RLE;
44          rleWriter = new RleWriter(encoding != PcxImageParser.PcxHeader.ENCODING_UNCOMPRESSED);
45          bitDepthWanted = params.getBitDepth();
46          planesWanted = params.getPlanes();
47          final PixelDensity pixelDensityParam = params.getPixelDensity();
48          // DPI is mandatory, so we have to invent something
49          pixelDensity = pixelDensityParam != null ? pixelDensityParam : PixelDensity.createFromPixelsPerInch(72, 72);
50      }
51  
52      public void writeImage(final BufferedImage src, final OutputStream os) throws IOException {
53          final PaletteFactory paletteFactory = new PaletteFactory();
54          final SimplePalette palette = paletteFactory.makeExactRgbPaletteSimple(src, 256);
55          @SuppressWarnings("resource") // Caller closes 'os'.
56          final AbstractBinaryOutputStream bos = AbstractBinaryOutputStream.littleEndian(os);
57          final int bitDepth;
58          final int planes;
59          if (palette == null || bitDepthWanted == 24 || bitDepthWanted == 32) {
60              if (bitDepthWanted == 32) {
61                  bitDepth = 32;
62                  planes = 1;
63              } else {
64                  bitDepth = 8;
65                  planes = 3;
66              }
67          } else if (palette.length() > 16 || bitDepthWanted == 8) {
68              bitDepth = 8;
69              planes = 1;
70          } else if (palette.length() > 8 || bitDepthWanted == 4) {
71              if (planesWanted == 1) {
72                  bitDepth = 4;
73                  planes = 1;
74              } else {
75                  bitDepth = 1;
76                  planes = 4;
77              }
78          } else if (palette.length() > 4 || bitDepthWanted == 3) {
79              bitDepth = 1;
80              planes = 3;
81          } else if (palette.length() > 2 || bitDepthWanted == 2) {
82              if (planesWanted == 2) {
83                  bitDepth = 1;
84                  planes = 2;
85              } else {
86                  bitDepth = 2;
87                  planes = 1;
88              }
89          } else {
90              boolean onlyBlackAndWhite = true;
91              if (palette.length() >= 1) {
92                  final int rgb = palette.getEntry(0);
93                  if (rgb != 0 && rgb != 0xffffff) {
94                      onlyBlackAndWhite = false;
95                  }
96              }
97              if (palette.length() == 2) {
98                  final int rgb = palette.getEntry(1);
99                  if (rgb != 0 && rgb != 0xffffff) {
100                     onlyBlackAndWhite = false;
101                 }
102             }
103             if (onlyBlackAndWhite) {
104                 bitDepth = 1;
105                 planes = 1;
106             } else {
107                 bitDepth = 1;
108                 planes = 2;
109             }
110         }
111 
112         int bytesPerLine = (bitDepth * src.getWidth() + 7) / 8;
113         if (bytesPerLine % 2 != 0) {
114             // must be even:
115             bytesPerLine++;
116         }
117 
118         final byte[] palette16 = new byte[16 * 3];
119         // TODO What's the right thing to do here for a null palette?
120         final int paletteLen = palette != null ? palette.length() : 0;
121         for (int i = 0; i < 16; i++) {
122             final int rgb;
123             if (i < paletteLen) {
124                 rgb = palette.getEntry(i);
125             } else {
126                 rgb = 0;
127             }
128             palette16[3 * i + 0] = (byte) (0xff & rgb >> 16);
129             palette16[3 * i + 1] = (byte) (0xff & rgb >> 8);
130             palette16[3 * i + 2] = (byte) (0xff & rgb);
131         }
132 
133         // PCX header
134         bos.write(10); // manufacturer
135         bos.write(bitDepth == 1 && planes == 1 ? 3 : 5); // version. Some apps only open black and white PCX with version=3.
136         bos.write(encoding); // encoding
137         bos.write(bitDepth); // bits per pixel
138         bos.write2Bytes(0); // xMin
139         bos.write2Bytes(0); // yMin
140         bos.write2Bytes(src.getWidth() - 1); // xMax
141         bos.write2Bytes(src.getHeight() - 1); // yMax
142         bos.write2Bytes((short) Math.round(pixelDensity.horizontalDensityInches())); // hDpi
143         bos.write2Bytes((short) Math.round(pixelDensity.verticalDensityInches())); // vDpi
144         bos.write(palette16); // 16 color palette
145         bos.write(0); // reserved
146         bos.write(planes); // planes
147         bos.write2Bytes(bytesPerLine); // bytes per line
148         bos.write2Bytes(1); // palette info
149         bos.write2Bytes(0); // hScreenSize
150         bos.write2Bytes(0); // vScreenSize
151         bos.write(new byte[54]);
152 
153         if (bitDepth == 32) {
154             writePixels32(src, bytesPerLine, bos);
155         } else {
156             writePixels(src, bitDepth, planes, bytesPerLine, palette, bos);
157         }
158 
159         if (bitDepth == 8 && planes == 1) {
160             // 256 color palette
161             bos.write(12);
162             for (int i = 0; i < 256; i++) {
163                 final int rgb;
164                 if (i < palette.length()) {
165                     rgb = palette.getEntry(i);
166                 } else {
167                     rgb = 0;
168                 }
169                 bos.write(rgb >> 16 & 0xff);
170                 bos.write(rgb >> 8 & 0xff);
171                 bos.write(rgb & 0xff);
172             }
173         }
174     }
175 
176     private void writePixels(final BufferedImage src, final int bitDepth, final int planes, final int bytesPerLine, final SimplePalette palette,
177             final AbstractBinaryOutputStream bos) throws IOException {
178         final byte[] plane0 = Allocator.byteArray(bytesPerLine);
179         final byte[] plane1 = Allocator.byteArray(bytesPerLine);
180         final byte[] plane2 = Allocator.byteArray(bytesPerLine);
181         final byte[] plane3 = Allocator.byteArray(bytesPerLine);
182         final byte[][] allPlanes = { plane0, plane1, plane2, plane3 };
183 
184         for (int y = 0; y < src.getHeight(); y++) {
185             for (int i = 0; i < planes; i++) {
186                 Arrays.fill(allPlanes[i], (byte) 0);
187             }
188 
189             if (bitDepth == 1 && planes == 1) {
190                 for (int x = 0; x < src.getWidth(); x++) {
191                     final int rgb = 0xffffff & src.getRGB(x, y);
192                     final int bit;
193                     if (rgb == 0x000000) {
194                         bit = 0;
195                     } else {
196                         bit = 1;
197                     }
198                     plane0[x >>> 3] |= bit << 7 - (x & 7);
199                 }
200             } else if (bitDepth == 1 && planes == 2) {
201                 for (int x = 0; x < src.getWidth(); x++) {
202                     final int argb = src.getRGB(x, y);
203                     final int index = palette.getPaletteIndex(0xffffff & argb);
204                     plane0[x >>> 3] |= (index & 1) << 7 - (x & 7);
205                     plane1[x >>> 3] |= (index & 2) >> 1 << 7 - (x & 7);
206                 }
207             } else if (bitDepth == 1 && planes == 3) {
208                 for (int x = 0; x < src.getWidth(); x++) {
209                     final int argb = src.getRGB(x, y);
210                     final int index = palette.getPaletteIndex(0xffffff & argb);
211                     plane0[x >>> 3] |= (index & 1) << 7 - (x & 7);
212                     plane1[x >>> 3] |= (index & 2) >> 1 << 7 - (x & 7);
213                     plane2[x >>> 3] |= (index & 4) >> 2 << 7 - (x & 7);
214                 }
215             } else if (bitDepth == 1 && planes == 4) {
216                 for (int x = 0; x < src.getWidth(); x++) {
217                     final int argb = src.getRGB(x, y);
218                     final int index = palette.getPaletteIndex(0xffffff & argb);
219                     plane0[x >>> 3] |= (index & 1) << 7 - (x & 7);
220                     plane1[x >>> 3] |= (index & 2) >> 1 << 7 - (x & 7);
221                     plane2[x >>> 3] |= (index & 4) >> 2 << 7 - (x & 7);
222                     plane3[x >>> 3] |= (index & 8) >> 3 << 7 - (x & 7);
223                 }
224             } else if (bitDepth == 2 && planes == 1) {
225                 for (int x = 0; x < src.getWidth(); x++) {
226                     final int argb = src.getRGB(x, y);
227                     final int index = palette.getPaletteIndex(0xffffff & argb);
228                     plane0[x >>> 2] |= index << 2 * (3 - (x & 3));
229                 }
230             } else if (bitDepth == 4 && planes == 1) {
231                 for (int x = 0; x < src.getWidth(); x++) {
232                     final int argb = src.getRGB(x, y);
233                     final int index = palette.getPaletteIndex(0xffffff & argb);
234                     plane0[x >>> 1] |= index << 4 * (1 - (x & 1));
235                 }
236             } else if (bitDepth == 8 && planes == 1) {
237                 for (int x = 0; x < src.getWidth(); x++) {
238                     final int argb = src.getRGB(x, y);
239                     final int index = palette.getPaletteIndex(0xffffff & argb);
240                     plane0[x] = (byte) index;
241                 }
242             } else if (bitDepth == 8 && planes == 3) {
243                 for (int x = 0; x < src.getWidth(); x++) {
244                     final int argb = src.getRGB(x, y);
245                     plane0[x] = (byte) (argb >>> 16);
246                     plane1[x] = (byte) (argb >>> 8);
247                     plane2[x] = (byte) argb;
248                 }
249             }
250 
251             for (int i = 0; i < planes; i++) {
252                 rleWriter.write(bos, allPlanes[i]);
253             }
254         }
255         rleWriter.flush(bos);
256     }
257 
258     private void writePixels32(final BufferedImage src, final int bytesPerLine, final AbstractBinaryOutputStream bos) throws IOException {
259 
260         final int[] rgbs = Allocator.intArray(src.getWidth());
261         final byte[] plane = Allocator.byteArray(4 * bytesPerLine);
262         for (int y = 0; y < src.getHeight(); y++) {
263             src.getRGB(0, y, src.getWidth(), 1, rgbs, 0, src.getWidth());
264             for (int x = 0; x < rgbs.length; x++) {
265                 plane[4 * x + 0] = (byte) rgbs[x];
266                 plane[4 * x + 1] = (byte) (rgbs[x] >> 8);
267                 plane[4 * x + 2] = (byte) (rgbs[x] >> 16);
268                 plane[4 * x + 3] = 0;
269             }
270             rleWriter.write(bos, plane);
271         }
272         rleWriter.flush(bos);
273     }
274 }