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.Allocator;
25  import org.apache.commons.imaging.common.BinaryOutputStream;
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          final BinaryOutputStream bos = BinaryOutputStream.littleEndian(os);
56          final int bitDepth;
57          final int planes;
58          if (palette == null || bitDepthWanted == 24 || bitDepthWanted == 32) {
59              if (bitDepthWanted == 32) {
60                  bitDepth = 32;
61                  planes = 1;
62              } else {
63                  bitDepth = 8;
64                  planes = 3;
65              }
66          } else if (palette.length() > 16 || bitDepthWanted == 8) {
67              bitDepth = 8;
68              planes = 1;
69          } else if (palette.length() > 8 || bitDepthWanted == 4) {
70              if (planesWanted == 1) {
71                  bitDepth = 4;
72                  planes = 1;
73              } else {
74                  bitDepth = 1;
75                  planes = 4;
76              }
77          } else if (palette.length() > 4 || bitDepthWanted == 3) {
78              bitDepth = 1;
79              planes = 3;
80          } else if (palette.length() > 2 || bitDepthWanted == 2) {
81              if (planesWanted == 2) {
82                  bitDepth = 1;
83                  planes = 2;
84              } else {
85                  bitDepth = 2;
86                  planes = 1;
87              }
88          } else {
89              boolean onlyBlackAndWhite = true;
90              if (palette.length() >= 1) {
91                  final int rgb = palette.getEntry(0);
92                  if (rgb != 0 && rgb != 0xffffff) {
93                      onlyBlackAndWhite = false;
94                  }
95              }
96              if (palette.length() == 2) {
97                  final int rgb = palette.getEntry(1);
98                  if (rgb != 0 && rgb != 0xffffff) {
99                      onlyBlackAndWhite = false;
100                 }
101             }
102             if (onlyBlackAndWhite) {
103                 bitDepth = 1;
104                 planes = 1;
105             } else {
106                 bitDepth = 1;
107                 planes = 2;
108             }
109         }
110 
111         int bytesPerLine = (bitDepth * src.getWidth() + 7) / 8;
112         if (bytesPerLine % 2 != 0) {
113             // must be even:
114             bytesPerLine++;
115         }
116 
117         final byte[] palette16 = new byte[16 * 3];
118         // TODO What's the right thing to do here for a null palette?
119         final int paletteLen = palette != null ? palette.length() : 0;
120         for (int i = 0; i < 16; i++) {
121             int rgb;
122             if (i < paletteLen) {
123                 rgb = palette.getEntry(i);
124             } else {
125                 rgb = 0;
126             }
127             palette16[3 * i + 0] = (byte) (0xff & rgb >> 16);
128             palette16[3 * i + 1] = (byte) (0xff & rgb >> 8);
129             palette16[3 * i + 2] = (byte) (0xff & rgb);
130         }
131 
132         // PCX header
133         bos.write(10); // manufacturer
134         bos.write(bitDepth == 1 && planes == 1 ? 3 : 5); // version. Some apps only open black and white PCX with version=3.
135         bos.write(encoding); // encoding
136         bos.write(bitDepth); // bits per pixel
137         bos.write2Bytes(0); // xMin
138         bos.write2Bytes(0); // yMin
139         bos.write2Bytes(src.getWidth() - 1); // xMax
140         bos.write2Bytes(src.getHeight() - 1); // yMax
141         bos.write2Bytes((short) Math.round(pixelDensity.horizontalDensityInches())); // hDpi
142         bos.write2Bytes((short) Math.round(pixelDensity.verticalDensityInches())); // vDpi
143         bos.write(palette16); // 16 color palette
144         bos.write(0); // reserved
145         bos.write(planes); // planes
146         bos.write2Bytes(bytesPerLine); // bytes per line
147         bos.write2Bytes(1); // palette info
148         bos.write2Bytes(0); // hScreenSize
149         bos.write2Bytes(0); // vScreenSize
150         bos.write(new byte[54]);
151 
152         if (bitDepth == 32) {
153             writePixels32(src, bytesPerLine, bos);
154         } else {
155             writePixels(src, bitDepth, planes, bytesPerLine, palette, bos);
156         }
157 
158         if (bitDepth == 8 && planes == 1) {
159             // 256 color palette
160             bos.write(12);
161             for (int i = 0; i < 256; i++) {
162                 int rgb;
163                 if (i < palette.length()) {
164                     rgb = palette.getEntry(i);
165                 } else {
166                     rgb = 0;
167                 }
168                 bos.write(rgb >> 16 & 0xff);
169                 bos.write(rgb >> 8 & 0xff);
170                 bos.write(rgb & 0xff);
171             }
172         }
173     }
174 
175     private void writePixels(final BufferedImage src, final int bitDepth, final int planes, final int bytesPerLine, final SimplePalette palette,
176             final BinaryOutputStream bos) throws IOException {
177         final byte[] plane0 = Allocator.byteArray(bytesPerLine);
178         final byte[] plane1 = Allocator.byteArray(bytesPerLine);
179         final byte[] plane2 = Allocator.byteArray(bytesPerLine);
180         final byte[] plane3 = Allocator.byteArray(bytesPerLine);
181         final byte[][] allPlanes = { plane0, plane1, plane2, plane3 };
182 
183         for (int y = 0; y < src.getHeight(); y++) {
184             for (int i = 0; i < planes; i++) {
185                 Arrays.fill(allPlanes[i], (byte) 0);
186             }
187 
188             if (bitDepth == 1 && planes == 1) {
189                 for (int x = 0; x < src.getWidth(); x++) {
190                     final int rgb = 0xffffff & src.getRGB(x, y);
191                     int bit;
192                     if (rgb == 0x000000) {
193                         bit = 0;
194                     } else {
195                         bit = 1;
196                     }
197                     plane0[x >>> 3] |= bit << 7 - (x & 7);
198                 }
199             } else if (bitDepth == 1 && planes == 2) {
200                 for (int x = 0; x < src.getWidth(); x++) {
201                     final int argb = src.getRGB(x, y);
202                     final int index = palette.getPaletteIndex(0xffffff & argb);
203                     plane0[x >>> 3] |= (index & 1) << 7 - (x & 7);
204                     plane1[x >>> 3] |= (index & 2) >> 1 << 7 - (x & 7);
205                 }
206             } else if (bitDepth == 1 && planes == 3) {
207                 for (int x = 0; x < src.getWidth(); x++) {
208                     final int argb = src.getRGB(x, y);
209                     final int index = palette.getPaletteIndex(0xffffff & argb);
210                     plane0[x >>> 3] |= (index & 1) << 7 - (x & 7);
211                     plane1[x >>> 3] |= (index & 2) >> 1 << 7 - (x & 7);
212                     plane2[x >>> 3] |= (index & 4) >> 2 << 7 - (x & 7);
213                 }
214             } else if (bitDepth == 1 && planes == 4) {
215                 for (int x = 0; x < src.getWidth(); x++) {
216                     final int argb = src.getRGB(x, y);
217                     final int index = palette.getPaletteIndex(0xffffff & argb);
218                     plane0[x >>> 3] |= (index & 1) << 7 - (x & 7);
219                     plane1[x >>> 3] |= (index & 2) >> 1 << 7 - (x & 7);
220                     plane2[x >>> 3] |= (index & 4) >> 2 << 7 - (x & 7);
221                     plane3[x >>> 3] |= (index & 8) >> 3 << 7 - (x & 7);
222                 }
223             } else if (bitDepth == 2 && planes == 1) {
224                 for (int x = 0; x < src.getWidth(); x++) {
225                     final int argb = src.getRGB(x, y);
226                     final int index = palette.getPaletteIndex(0xffffff & argb);
227                     plane0[x >>> 2] |= index << 2 * (3 - (x & 3));
228                 }
229             } else if (bitDepth == 4 && planes == 1) {
230                 for (int x = 0; x < src.getWidth(); x++) {
231                     final int argb = src.getRGB(x, y);
232                     final int index = palette.getPaletteIndex(0xffffff & argb);
233                     plane0[x >>> 1] |= index << 4 * (1 - (x & 1));
234                 }
235             } else if (bitDepth == 8 && planes == 1) {
236                 for (int x = 0; x < src.getWidth(); x++) {
237                     final int argb = src.getRGB(x, y);
238                     final int index = palette.getPaletteIndex(0xffffff & argb);
239                     plane0[x] = (byte) index;
240                 }
241             } else if (bitDepth == 8 && planes == 3) {
242                 for (int x = 0; x < src.getWidth(); x++) {
243                     final int argb = src.getRGB(x, y);
244                     plane0[x] = (byte) (argb >>> 16);
245                     plane1[x] = (byte) (argb >>> 8);
246                     plane2[x] = (byte) argb;
247                 }
248             }
249 
250             for (int i = 0; i < planes; i++) {
251                 rleWriter.write(bos, allPlanes[i]);
252             }
253         }
254         rleWriter.flush(bos);
255     }
256 
257     private void writePixels32(final BufferedImage src, final int bytesPerLine, final BinaryOutputStream bos) throws IOException {
258 
259         final int[] rgbs = Allocator.intArray(src.getWidth());
260         final byte[] plane = Allocator.byteArray(4 * bytesPerLine);
261         for (int y = 0; y < src.getHeight(); y++) {
262             src.getRGB(0, y, src.getWidth(), 1, rgbs, 0, src.getWidth());
263             for (int x = 0; x < rgbs.length; x++) {
264                 plane[4 * x + 0] = (byte) rgbs[x];
265                 plane[4 * x + 1] = (byte) (rgbs[x] >> 8);
266                 plane[4 * x + 2] = (byte) (rgbs[x] >> 16);
267                 plane[4 * x + 3] = 0;
268             }
269             rleWriter.write(bos, plane);
270         }
271         rleWriter.flush(bos);
272     }
273 }