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.BinaryOutputStream;
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 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 @Override
102 public boolean dumpImageFile(final PrintWriter pw, final ByteSource byteSource) throws ImagingException, IOException {
103 final IcnsContents icnsContents = readImage(byteSource);
104 icnsContents.icnsHeader.dump(pw);
105 for (final IcnsElement icnsElement : icnsContents.icnsElements) {
106 icnsElement.dump(pw);
107 }
108 return true;
109 }
110
111 @Override
112 protected String[] getAcceptedExtensions() {
113 return ACCEPTED_EXTENSIONS;
114 }
115
116 @Override
117 protected ImageFormat[] getAcceptedTypes() {
118 return new ImageFormat[] { ImageFormats.ICNS };
119 }
120
121 @Override
122 public List<BufferedImage> getAllBufferedImages(final ByteSource byteSource) throws ImagingException, IOException {
123 final IcnsContents icnsContents = readImage(byteSource);
124 return IcnsDecoder.decodeAllImages(icnsContents.icnsElements);
125 }
126
127 @Override
128 public final BufferedImage getBufferedImage(final ByteSource byteSource, final IcnsImagingParameters params) throws ImagingException, IOException {
129 final IcnsContents icnsContents = readImage(byteSource);
130 final List<BufferedImage> result = IcnsDecoder.decodeAllImages(icnsContents.icnsElements);
131 if (!result.isEmpty()) {
132 return result.get(0);
133 }
134 throw new ImagingException("No icons in ICNS file");
135 }
136
137 @Override
138 public String getDefaultExtension() {
139 return DEFAULT_EXTENSION;
140 }
141
142 @Override
143 public IcnsImagingParameters getDefaultParameters() {
144 return new IcnsImagingParameters();
145 }
146
147 @Override
148 public byte[] getIccProfileBytes(final ByteSource byteSource, final IcnsImagingParameters params) throws ImagingException, IOException {
149 return null;
150 }
151
152 @Override
153 public ImageInfo getImageInfo(final ByteSource byteSource, final IcnsImagingParameters params) throws ImagingException, IOException {
154 final IcnsContents contents = readImage(byteSource);
155 final List<BufferedImage> images = IcnsDecoder.decodeAllImages(contents.icnsElements);
156 if (images.isEmpty()) {
157 throw new ImagingException("No icons in ICNS file");
158 }
159 final BufferedImage image0 = images.get(0);
160 return new ImageInfo("Icns", 32, new ArrayList<>(), ImageFormats.ICNS, "ICNS Apple Icon Image", image0.getHeight(), "image/x-icns", images.size(), 0, 0,
161 0, 0, image0.getWidth(), false, true, false, ImageInfo.ColorType.RGB, ImageInfo.CompressionAlgorithm.UNKNOWN);
162 }
163
164 @Override
165 public Dimension getImageSize(final ByteSource byteSource, final IcnsImagingParameters params) throws ImagingException, IOException {
166 final IcnsContents contents = readImage(byteSource);
167 final List<BufferedImage> images = IcnsDecoder.decodeAllImages(contents.icnsElements);
168 if (images.isEmpty()) {
169 throw new ImagingException("No icons in ICNS file");
170 }
171 final BufferedImage image0 = images.get(0);
172 return new Dimension(image0.getWidth(), image0.getHeight());
173 }
174
175
176 @Override
177 public ImageMetadata getMetadata(final ByteSource byteSource, final IcnsImagingParameters params) throws ImagingException, IOException {
178 return null;
179 }
180
181 @Override
182 public String getName() {
183 return "Apple Icon Image";
184 }
185
186 private IcnsElement readIcnsElement(final InputStream is, final int remainingSize) throws IOException {
187
188 final int type = read4Bytes("Type", is, "Not a valid ICNS file", getByteOrder());
189
190 final int elementSize = read4Bytes("ElementSize", is, "Not a valid ICNS file", getByteOrder());
191 if (elementSize > remainingSize) {
192 throw new IOException(String.format("Corrupted ICNS file: element size %d is greater than " + "remaining size %d", elementSize, remainingSize));
193 }
194 final byte[] data = readBytes("Data", is, elementSize - 8, "Not a valid ICNS file");
195
196 return new IcnsElement(type, elementSize, data);
197 }
198
199 private IcnsHeader readIcnsHeader(final InputStream is) throws ImagingException, IOException {
200 final int magic = read4Bytes("Magic", is, "Not a Valid ICNS File", getByteOrder());
201 final int fileSize = read4Bytes("FileSize", is, "Not a Valid ICNS File", getByteOrder());
202
203 if (magic != ICNS_MAGIC) {
204 throw new ImagingException("Not a Valid ICNS File: " + "magic is 0x" + Integer.toHexString(magic));
205 }
206
207 return new IcnsHeader(magic, fileSize);
208 }
209
210 private IcnsContents readImage(final ByteSource byteSource) throws ImagingException, IOException {
211 try (InputStream is = byteSource.getInputStream()) {
212 final IcnsHeader icnsHeader = readIcnsHeader(is);
213
214 final List<IcnsElement> icnsElementList = new ArrayList<>();
215 for (int remainingSize = icnsHeader.fileSize - 8; remainingSize > 0;) {
216 final IcnsElement icnsElement = readIcnsElement(is, remainingSize);
217 icnsElementList.add(icnsElement);
218 remainingSize -= icnsElement.elementSize;
219 }
220
221 return new IcnsContents(icnsHeader, icnsElementList.toArray(IcnsElement.EMPTY_ARRAY));
222 }
223 }
224
225 @Override
226 public void writeImage(final BufferedImage src, final OutputStream os, final IcnsImagingParameters params) throws ImagingException, IOException {
227 IcnsType imageType;
228 if (src.getWidth() == 16 && src.getHeight() == 16) {
229 imageType = IcnsType.ICNS_16x16_32BIT_IMAGE;
230 } else if (src.getWidth() == 32 && src.getHeight() == 32) {
231 imageType = IcnsType.ICNS_32x32_32BIT_IMAGE;
232 } else if (src.getWidth() == 48 && src.getHeight() == 48) {
233 imageType = IcnsType.ICNS_48x48_32BIT_IMAGE;
234 } else if (src.getWidth() == 128 && src.getHeight() == 128) {
235 imageType = IcnsType.ICNS_128x128_32BIT_IMAGE;
236 } else {
237 throw new ImagingException("Invalid/unsupported source width " + src.getWidth() + " and height " + src.getHeight());
238 }
239
240 try (BinaryOutputStream bos = BinaryOutputStream.bigEndian(os)) {
241 bos.write4Bytes(ICNS_MAGIC);
242 bos.write4Bytes(4 + 4 + 4 + 4 + 4 * imageType.getWidth() * imageType.getHeight() + 4 + 4 + imageType.getWidth() * imageType.getHeight());
243
244 bos.write4Bytes(imageType.getType());
245 bos.write4Bytes(4 + 4 + 4 * imageType.getWidth() * imageType.getHeight());
246 for (int y = 0; y < src.getHeight(); y++) {
247 for (int x = 0; x < src.getWidth(); x++) {
248 final int argb = src.getRGB(x, y);
249 bos.write(0);
250 bos.write(argb >> 16);
251 bos.write(argb >> 8);
252 bos.write(argb);
253 }
254 }
255
256 final IcnsType maskType = IcnsType.find8BPPMaskType(imageType);
257 bos.write4Bytes(maskType.getType());
258 bos.write4Bytes(4 + 4 + imageType.getWidth() * imageType.getWidth());
259 for (int y = 0; y < src.getHeight(); y++) {
260 for (int x = 0; x < src.getWidth(); x++) {
261 final int argb = src.getRGB(x, y);
262 bos.write(argb >> 24);
263 }
264 }
265 }
266 }
267 }