1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.apache.commons.imaging.formats.icns;
18
19 import static org.apache.commons.imaging.common.BinaryFunctions.read4Bytes;
20 import static org.apache.commons.imaging.common.BinaryFunctions.readBytes;
21
22 import java.awt.Dimension;
23 import java.awt.image.BufferedImage;
24 import java.io.IOException;
25 import java.io.InputStream;
26 import java.io.OutputStream;
27 import java.io.PrintWriter;
28 import java.util.ArrayList;
29 import java.util.List;
30
31 import org.apache.commons.imaging.AbstractImageParser;
32 import org.apache.commons.imaging.ImageFormat;
33 import org.apache.commons.imaging.ImageFormats;
34 import org.apache.commons.imaging.ImageInfo;
35 import org.apache.commons.imaging.ImagingException;
36 import org.apache.commons.imaging.bytesource.ByteSource;
37 import org.apache.commons.imaging.common.AbstractBinaryOutputStream;
38 import org.apache.commons.imaging.common.ImageMetadata;
39
40 public class IcnsImageParser extends AbstractImageParser<IcnsImagingParameters> {
41 private static final class IcnsContents {
42 public final IcnsHeader icnsHeader;
43 public final IcnsElement[] icnsElements;
44
45 IcnsContents(final IcnsHeader icnsHeader, final IcnsElement[] icnsElements) {
46 this.icnsHeader = icnsHeader;
47 this.icnsElements = icnsElements;
48 }
49 }
50
51 static class IcnsElement {
52 static final IcnsElement[] EMPTY_ARRAY = {};
53 public final int type;
54 public final int elementSize;
55 public final byte[] data;
56
57 IcnsElement(final int type, final int elementSize, final byte[] data) {
58 this.type = type;
59 this.elementSize = elementSize;
60 this.data = data;
61 }
62
63 public void dump(final PrintWriter pw) {
64 pw.println("IcnsElement");
65 final IcnsType icnsType = IcnsType.findAnyType(type);
66 final String typeDescription;
67 if (icnsType == null) {
68 typeDescription = "";
69 } else {
70 typeDescription = " " + icnsType.toString();
71 }
72 pw.println("Type: 0x" + Integer.toHexString(type) + " (" + IcnsType.describeType(type) + ")" + typeDescription);
73 pw.println("ElementSize: " + elementSize);
74 pw.println("");
75 }
76 }
77
78 private static final class IcnsHeader {
79 public final int magic;
80 public final int fileSize;
81
82 IcnsHeader(final int magic, final int fileSize) {
83 this.magic = magic;
84 this.fileSize = fileSize;
85 }
86
87 public void dump(final PrintWriter pw) {
88 pw.println("IcnsHeader");
89 pw.println("Magic: 0x" + Integer.toHexString(magic) + " (" + IcnsType.describeType(magic) + ")");
90 pw.println("FileSize: " + fileSize);
91 pw.println("");
92 }
93 }
94
95 static final int ICNS_MAGIC = IcnsType.typeAsInt("icns");
96
97 private static final String DEFAULT_EXTENSION = ImageFormats.ICNS.getDefaultExtension();
98
99 private static final String[] ACCEPTED_EXTENSIONS = ImageFormats.ICNS.getExtensions();
100
101
102
103
104 public IcnsImageParser() {
105
106 }
107
108 @Override
109 public boolean dumpImageFile(final PrintWriter pw, final ByteSource byteSource) throws ImagingException, IOException {
110 final IcnsContents icnsContents = readImage(byteSource);
111 icnsContents.icnsHeader.dump(pw);
112 for (final IcnsElement icnsElement : icnsContents.icnsElements) {
113 icnsElement.dump(pw);
114 }
115 return true;
116 }
117
118 @Override
119 protected String[] getAcceptedExtensions() {
120 return ACCEPTED_EXTENSIONS;
121 }
122
123 @Override
124 protected ImageFormat[] getAcceptedTypes() {
125 return new ImageFormat[] { ImageFormats.ICNS };
126 }
127
128 @Override
129 public List<BufferedImage> getAllBufferedImages(final ByteSource byteSource) throws ImagingException, IOException {
130 final IcnsContents icnsContents = readImage(byteSource);
131 return IcnsDecoder.decodeAllImages(icnsContents.icnsElements);
132 }
133
134 @Override
135 public final BufferedImage getBufferedImage(final ByteSource byteSource, final IcnsImagingParameters params) throws ImagingException, IOException {
136 final IcnsContents icnsContents = readImage(byteSource);
137 final List<BufferedImage> result = IcnsDecoder.decodeAllImages(icnsContents.icnsElements);
138 if (!result.isEmpty()) {
139 return result.get(0);
140 }
141 throw new ImagingException("No icons in ICNS file");
142 }
143
144 @Override
145 public String getDefaultExtension() {
146 return DEFAULT_EXTENSION;
147 }
148
149 @Override
150 public IcnsImagingParameters getDefaultParameters() {
151 return new IcnsImagingParameters();
152 }
153
154 @Override
155 public byte[] getIccProfileBytes(final ByteSource byteSource, final IcnsImagingParameters params) throws ImagingException, IOException {
156 return null;
157 }
158
159 @Override
160 public ImageInfo getImageInfo(final ByteSource byteSource, final IcnsImagingParameters params) throws ImagingException, IOException {
161 final IcnsContents contents = readImage(byteSource);
162 final List<BufferedImage> images = IcnsDecoder.decodeAllImages(contents.icnsElements);
163 if (images.isEmpty()) {
164 throw new ImagingException("No icons in ICNS file");
165 }
166 final BufferedImage image0 = images.get(0);
167 return new ImageInfo("Icns", 32, new ArrayList<>(), ImageFormats.ICNS, "ICNS Apple Icon Image", image0.getHeight(), "image/x-icns", images.size(), 0, 0,
168 0, 0, image0.getWidth(), false, true, false, ImageInfo.ColorType.RGB, ImageInfo.CompressionAlgorithm.UNKNOWN);
169 }
170
171 @Override
172 public Dimension getImageSize(final ByteSource byteSource, final IcnsImagingParameters params) throws ImagingException, IOException {
173 final IcnsContents contents = readImage(byteSource);
174 final List<BufferedImage> images = IcnsDecoder.decodeAllImages(contents.icnsElements);
175 if (images.isEmpty()) {
176 throw new ImagingException("No icons in ICNS file");
177 }
178 final BufferedImage image0 = images.get(0);
179 return new Dimension(image0.getWidth(), image0.getHeight());
180 }
181
182
183 @Override
184 public ImageMetadata getMetadata(final ByteSource byteSource, final IcnsImagingParameters params) throws ImagingException, IOException {
185 return null;
186 }
187
188 @Override
189 public String getName() {
190 return "Apple Icon Image";
191 }
192
193 private IcnsElement readIcnsElement(final InputStream is, final int remainingSize) throws IOException {
194
195 final int type = read4Bytes("Type", is, "Not a valid ICNS file", getByteOrder());
196
197 final int elementSize = read4Bytes("ElementSize", is, "Not a valid ICNS file", getByteOrder());
198 if (elementSize > remainingSize) {
199 throw new IOException(String.format("Corrupted ICNS file: element size %d is greater than " + "remaining size %d", elementSize, remainingSize));
200 }
201 final byte[] data = readBytes("Data", is, elementSize - 8, "Not a valid ICNS file");
202
203 return new IcnsElement(type, elementSize, data);
204 }
205
206 private IcnsHeader readIcnsHeader(final InputStream is) throws ImagingException, IOException {
207 final int magic = read4Bytes("Magic", is, "Not a Valid ICNS File", getByteOrder());
208 final int fileSize = read4Bytes("FileSize", is, "Not a Valid ICNS File", getByteOrder());
209
210 if (magic != ICNS_MAGIC) {
211 throw new ImagingException("Not a Valid ICNS File: " + "magic is 0x" + Integer.toHexString(magic));
212 }
213
214 return new IcnsHeader(magic, fileSize);
215 }
216
217 private IcnsContents readImage(final ByteSource byteSource) throws ImagingException, IOException {
218 try (InputStream is = byteSource.getInputStream()) {
219 final IcnsHeader icnsHeader = readIcnsHeader(is);
220
221 final List<IcnsElement> icnsElementList = new ArrayList<>();
222 for (int remainingSize = icnsHeader.fileSize - 8; remainingSize > 0;) {
223 final IcnsElement icnsElement = readIcnsElement(is, remainingSize);
224 icnsElementList.add(icnsElement);
225 remainingSize -= icnsElement.elementSize;
226 }
227
228 return new IcnsContents(icnsHeader, icnsElementList.toArray(IcnsElement.EMPTY_ARRAY));
229 }
230 }
231
232 @Override
233 public void writeImage(final BufferedImage src, final OutputStream os, final IcnsImagingParameters params) throws ImagingException, IOException {
234 final IcnsType imageType;
235 if (src.getWidth() == 16 && src.getHeight() == 16) {
236 imageType = IcnsType.ICNS_16x16_32BIT_IMAGE;
237 } else if (src.getWidth() == 32 && src.getHeight() == 32) {
238 imageType = IcnsType.ICNS_32x32_32BIT_IMAGE;
239 } else if (src.getWidth() == 48 && src.getHeight() == 48) {
240 imageType = IcnsType.ICNS_48x48_32BIT_IMAGE;
241 } else if (src.getWidth() == 128 && src.getHeight() == 128) {
242 imageType = IcnsType.ICNS_128x128_32BIT_IMAGE;
243 } else {
244 throw new ImagingException("Invalid/unsupported source width " + src.getWidth() + " and height " + src.getHeight());
245 }
246
247 try (AbstractBinaryOutputStream bos = AbstractBinaryOutputStream.bigEndian(os)) {
248 bos.write4Bytes(ICNS_MAGIC);
249 bos.write4Bytes(4 + 4 + 4 + 4 + 4 * imageType.getWidth() * imageType.getHeight() + 4 + 4 + imageType.getWidth() * imageType.getHeight());
250
251 bos.write4Bytes(imageType.getType());
252 bos.write4Bytes(4 + 4 + 4 * imageType.getWidth() * imageType.getHeight());
253 for (int y = 0; y < src.getHeight(); y++) {
254 for (int x = 0; x < src.getWidth(); x++) {
255 final int argb = src.getRGB(x, y);
256 bos.write(0);
257 bos.write(argb >> 16);
258 bos.write(argb >> 8);
259 bos.write(argb);
260 }
261 }
262
263 final IcnsType maskType = IcnsType.find8BPPMaskType(imageType);
264 bos.write4Bytes(maskType.getType());
265 bos.write4Bytes(4 + 4 + imageType.getWidth() * imageType.getWidth());
266 for (int y = 0; y < src.getHeight(); y++) {
267 for (int x = 0; x < src.getWidth(); x++) {
268 final int argb = src.getRGB(x, y);
269 bos.write(argb >> 24);
270 }
271 }
272 }
273 }
274 }