#83 ICNS write

This commit is contained in:
Harald Kuhr 2018-08-25 15:34:07 +02:00
parent b09a8a5dbe
commit 96901e020f
9 changed files with 695 additions and 30 deletions

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2011, Harald Kuhr * Copyright (c) 2017, Harald Kuhr
* All rights reserved. * All rights reserved.
* *
* Redistribution and use in source and binary forms, with or without * Redistribution and use in source and binary forms, with or without
@ -92,14 +92,28 @@ interface ICNS {
/** 128×128 8-bit mask. */ /** 128×128 8-bit mask. */
int t8mk = ('t' << 24) + ('8' << 16) + ('m' << 8) + 'k'; int t8mk = ('t' << 24) + ('8' << 16) + ('m' << 8) + 'k';
/** 256×256 JPEG 2000 or PNG icon (10.x+). */ /** 16x16 JPEG2000 or PNG icon (10.7+). */
int icp4 = ('i' << 24) + ('c' << 16) + ('p' << 8) + '4';
/** 32x32 JPEG2000 or PNG icon (10.7+). */
int icp5 = ('i' << 24) + ('c' << 16) + ('p' << 8) + '5';
/** 64x64 JPEG2000 or PNG icon (10.7+). */
int icp6 = ('i' << 24) + ('c' << 16) + ('p' << 8) + '6';
/** 128x128 JPEG2000 or PNG icon (10.7+). */
int ic07 = ('i' << 24) + ('c' << 16) + ('0' << 8) + '7';
/** 256×256 JPEG 2000 or PNG icon (10.5+). */
int ic08 = ('i' << 24) + ('c' << 16) + ('0' << 8) + '8'; int ic08 = ('i' << 24) + ('c' << 16) + ('0' << 8) + '8';
/** 512×512 JPEG 2000 or PNG icon (10.5+). */
/** 512×512 JPEG 2000 or PNG icon (10.x+). */
int ic09 = ('i' << 24) + ('c' << 16) + ('0' << 8) + '9'; int ic09 = ('i' << 24) + ('c' << 16) + ('0' << 8) + '9';
/** 1024×1024 JPEG2000 or PNG icon (10.7+) OR 512x512@2x "retina" (10.8+). */
/** 1024×1024 PNG icon (10.7+). */
int ic10 = ('i' << 24) + ('c' << 16) + ('1' << 8) + '0'; int ic10 = ('i' << 24) + ('c' << 16) + ('1' << 8) + '0';
/** 16x16@2x "retina" JPEG2000 or PNG icon (10.8+). */
int ic11 = ('i' << 24) + ('c' << 16) + ('1' << 8) + '1';
/** 32x32@2x "retina" JPEG2000 or PNG icon (10.8+). */
int ic12 = ('i' << 24) + ('c' << 16) + ('1' << 8) + '2';
/** 128x128@2x "retina" JPEG2000 or PNG icon (10.8+). */
int ic13 = ('i' << 24) + ('c' << 16) + ('1' << 8) + '3';
/** 256x256@2x "retina" JPEG2000 or PNG icon (10.8+). */
int ic14 = ('i' << 24) + ('c' << 16) + ('1' << 8) + '4';
/** Unknown (Version). */ /** Unknown (Version). */
int icnV = ('i' << 24) + ('c' << 16) + ('n' << 8) + 'V'; int icnV = ('i' << 24) + ('c' << 16) + ('n' << 8) + 'V';

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2011, Harald Kuhr * Copyright (c) 2017, Harald Kuhr
* All rights reserved. * All rights reserved.
* *
* Redistribution and use in source and binary forms, with or without * Redistribution and use in source and binary forms, with or without
@ -31,6 +31,7 @@
package com.twelvemonkeys.imageio.plugins.icns; package com.twelvemonkeys.imageio.plugins.icns;
import com.twelvemonkeys.imageio.ImageReaderBase; import com.twelvemonkeys.imageio.ImageReaderBase;
import com.twelvemonkeys.imageio.stream.SubImageInputStream;
import com.twelvemonkeys.imageio.util.IIOUtil; import com.twelvemonkeys.imageio.util.IIOUtil;
import com.twelvemonkeys.imageio.util.ImageTypeSpecifiers; import com.twelvemonkeys.imageio.util.ImageTypeSpecifiers;
@ -422,7 +423,7 @@ public final class ICNSImageReader extends ImageReaderBase {
private BufferedImage readForeignFormat(int imageIndex, final ImageReadParam param, final IconResource resource) throws IOException { private BufferedImage readForeignFormat(int imageIndex, final ImageReadParam param, final IconResource resource) throws IOException {
// TODO: Optimize by caching readers that work? // TODO: Optimize by caching readers that work?
ImageInputStream stream = ImageIO.createImageInputStream(IIOUtil.createStreamAdapter(imageInput, resource.length)); ImageInputStream stream = new SubImageInputStream(imageInput, resource.length);
try { try {
// Try first using ImageIO // Try first using ImageIO
@ -441,7 +442,7 @@ public final class ICNSImageReader extends ImageReaderBase {
} }
else { else {
stream.close(); stream.close();
stream = ImageIO.createImageInputStream(IIOUtil.createStreamAdapter(imageInput, resource.length)); stream = new SubImageInputStream(imageInput, resource.length);
} }
} }
finally { finally {
@ -524,10 +525,28 @@ public final class ICNSImageReader extends ImageReaderBase {
} }
IconResource resource = IconResource.read(imageInput); IconResource resource = IconResource.read(imageInput);
// System.err.println("resource: " + resource);
if (resource.isTOC()) {
// TODO: IconResource.readTOC()?
int resourceCount = (resource.length - ICNS.RESOURCE_HEADER_SIZE) / ICNS.RESOURCE_HEADER_SIZE;
long pos = resource.start + resource.length;
for (int i = 0; i < resourceCount; i++) {
resource = IconResource.read(pos, imageInput);
pos += resource.length;
addResource(resource);
}
}
else {
addResource(resource);
}
lastResourceRead = resource; lastResourceRead = resource;
return resource;
}
private void addResource(final IconResource resource) {
// Filter out special cases like 'icnV' or 'TOC ' resources // Filter out special cases like 'icnV' or 'TOC ' resources
if (resource.isMaskType()) { if (resource.isMaskType()) {
masks.add(resource); masks.add(resource);
@ -535,8 +554,6 @@ public final class ICNSImageReader extends ImageReaderBase {
else if (!resource.isUnknownType()) { else if (!resource.isUnknownType()) {
icons.add(resource); icons.add(resource);
} }
return resource;
} }
private void readeFileHeader() throws IOException { private void readeFileHeader() throws IOException {

View File

@ -0,0 +1,273 @@
/*
* Copyright (c) 2017, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.imageio.plugins.icns;
import com.twelvemonkeys.imageio.ImageWriterBase;
import com.twelvemonkeys.imageio.stream.SubImageOutputStream;
import com.twelvemonkeys.imageio.util.ProgressListenerBase;
import javax.imageio.*;
import javax.imageio.event.IIOWriteWarningListener;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.spi.ImageWriterSpi;
import javax.imageio.stream.ImageInputStream;
import javax.imageio.stream.ImageOutputStream;
import java.io.File;
import java.io.IOException;
import java.util.Iterator;
/**
* ICNSImageWriter
*/
public final class ICNSImageWriter extends ImageWriterBase {
private int sequenceIndex = -1;
private ImageWriter pngDelegate;
ICNSImageWriter(ImageWriterSpi provider) {
super(provider);
}
@Override
protected void resetMembers() {
sequenceIndex = -1;
if (pngDelegate != null) {
pngDelegate.dispose();
pngDelegate = null;
}
}
@Override
public IIOMetadata getDefaultImageMetadata(final ImageTypeSpecifier imageType, final ImageWriteParam param) {
return null;
}
@Override
public IIOMetadata convertImageMetadata(final IIOMetadata inData, final ImageTypeSpecifier imageType, final ImageWriteParam param) {
return null;
}
@Override
public void write(final IIOMetadata streamMetadata, final IIOImage image, final ImageWriteParam param) throws IOException {
prepareWriteSequence(streamMetadata);
writeToSequence(image, param);
endWriteSequence();
}
@Override
public boolean canWriteSequence() {
return true;
}
@Override
public void prepareWriteSequence(final IIOMetadata streamMetadata) throws IOException {
assertOutput();
// TODO: Allow TOC resource to be passed as stream metadata?
// - We only need number of icons to be written later
// - The contents of the TOC could be updated while adding to the sequence
if (sequenceIndex >= 0) {
throw new IllegalStateException("writeSequence already started");
}
writeICNSHeader();
sequenceIndex = 0;
}
@Override
public void endWriteSequence() throws IOException {
assertOutput();
if (sequenceIndex < 0) {
throw new IllegalStateException("prepareWriteSequence not called");
}
// TODO: Now that we know the number of icon resources, we could move all data backwards
// and write a TOC... But I don't think the benefit will outweigh the cost.
sequenceIndex = -1;
}
@Override
public void writeToSequence(final IIOImage image, final ImageWriteParam param) throws IOException {
assertOutput();
if (sequenceIndex < 0) {
throw new IllegalStateException("prepareWriteSequence not called");
}
if (image.hasRaster()) {
throw new UnsupportedOperationException("image has a Raster");
}
long resourceStart = imageOutput.getStreamPosition();
// TODO: Allow for other formats based on param?
// - Uncompressed/RLE (only allowed up to 128x128)?
// - JPEG2000 not very likely...
// Validate icon size, get icon resource type based on size and compression
// TODO: Allow smaller, centered in larger square? Resize?
imageOutput.writeInt(IconResource.typeFromImage(image.getRenderedImage(), "PNG"));
imageOutput.writeInt(0); // Size, update later
processImageStarted(sequenceIndex);
// Write icon in PNG format
ImageWriter writer = getPNGDelegate();
writer.setOutput(new SubImageOutputStream(imageOutput));
writer.write(null, image, copyParam(param, writer));
processImageComplete();
long resourceEnd = imageOutput.getStreamPosition();
if (resourceEnd > Integer.MAX_VALUE) {
throw new IIOException("File too large for ICNS");
}
int length = (int) (resourceEnd - resourceStart);
// Update file length field
imageOutput.seek(4);
imageOutput.writeInt((int) resourceEnd);
// Update resource length field
imageOutput.seek(resourceStart + 4);
imageOutput.writeInt((length));
// Prepare for next iteration
imageOutput.seek(resourceEnd);
}
private ImageWriteParam copyParam(final ImageWriteParam param, ImageWriter writer) {
if (param == null) {
return null;
}
ImageWriteParam writeParam = writer.getDefaultWriteParam();
writeParam.setSourceSubsampling(param.getSourceXSubsampling(), param.getSourceYSubsampling(), param.getSubsamplingXOffset(), param.getSubsamplingYOffset());
writeParam.setSourceRegion(param.getSourceRegion());
writeParam.setSourceBands(param.getSourceBands());
return writeParam;
}
private ImageWriter getPNGDelegate() {
if (pngDelegate == null) {
// There's always a PNG writer...
pngDelegate = ImageIO.getImageWritersByFormatName("PNG").next();
pngDelegate.setLocale(getLocale());
pngDelegate.addIIOWriteProgressListener(new ProgressListenerBase() {
@Override
public void imageProgress(ImageWriter source, float percentageDone) {
processImageProgress(percentageDone);
}
@Override
public void writeAborted(ImageWriter source) {
processWriteAborted();
}
});
pngDelegate.addIIOWriteWarningListener(new IIOWriteWarningListener() {
@Override
public void warningOccurred(ImageWriter source, int imageIndex, String warning) {
processWarningOccurred(sequenceIndex, warning);
}
});
}
return pngDelegate;
}
private void writeICNSHeader() throws IOException {
if (imageOutput.getStreamPosition() != 0) {
throw new IllegalStateException("Stream already written to");
}
imageOutput.writeInt(ICNS.MAGIC);
imageOutput.writeInt(8); // Length of file, in bytes, must be updated while writing
}
public static void main(String[] args) throws IOException {
boolean pngCompression = false;
int firstArg = 0;
while (args.length > firstArg && args[firstArg].charAt(0) == '-') {
if (args[firstArg].equals("-p") || args[firstArg].equals("--png")) {
pngCompression = true;
}
firstArg++;
}
if (args.length - firstArg < 2) {
System.err.println("Usage: command [-p|--png] <output.ico> <input> [<input>...]");
System.exit(1);
}
try (ImageOutputStream out = ImageIO.createImageOutputStream(new File(args[firstArg++]))) {
ImageWriter writer = new ICNSImageWriter(null);
writer.setOutput(out);
ImageWriteParam param = writer.getDefaultWriteParam();
// For now, we only support PNG...
// param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
// param.setCompressionType(pngCompression ? "BI_PNG" : "BI_RGB");
writer.prepareWriteSequence(null);
for (int i = firstArg; i < args.length; i++) {
File inFile = new File(args[i]);
try (ImageInputStream input = ImageIO.createImageInputStream(inFile)) {
Iterator<ImageReader> readers = ImageIO.getImageReaders(input);
if (!readers.hasNext()) {
System.err.printf("Can't read %s\n", inFile.getAbsolutePath());
}
else {
ImageReader reader = readers.next();
reader.setInput(input);
for (int j = 0; j < reader.getNumImages(true); j++) {
IIOImage image = reader.readAll(j, null);
writer.writeToSequence(image, param);
}
}
}
}
writer.endWriteSequence();
writer.dispose();
}
}
}

View File

@ -0,0 +1,60 @@
/*
* Copyright (c) 2017, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.imageio.plugins.icns;
import com.twelvemonkeys.imageio.spi.ImageWriterSpiBase;
import javax.imageio.ImageTypeSpecifier;
import java.util.Locale;
/**
* ICNSImageWriterSpi
*/
public final class ICNSImageWriterSpi extends ImageWriterSpiBase {
public ICNSImageWriterSpi() {
super(new ICNSProviderInfo());
}
@Override
public boolean canEncodeImage(final ImageTypeSpecifier type) {
return true;
}
@Override
public ICNSImageWriter createWriterInstance(final Object extension) {
return new ICNSImageWriter(this);
}
@Override
public String getDescription(final Locale locale) {
return "Apple Icon Image (icns) format Writer";
}
}

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2015, Harald Kuhr * Copyright (c) 2017, Harald Kuhr
* All rights reserved. * All rights reserved.
* *
* Redistribution and use in source and binary forms, with or without * Redistribution and use in source and binary forms, with or without
@ -40,7 +40,7 @@ import com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfo;
* @version $Id: ICNSProviderInfo.java,v 1.0 20/03/15 harald.kuhr Exp$ * @version $Id: ICNSProviderInfo.java,v 1.0 20/03/15 harald.kuhr Exp$
*/ */
final class ICNSProviderInfo extends ReaderWriterProviderInfo { final class ICNSProviderInfo extends ReaderWriterProviderInfo {
protected ICNSProviderInfo() { ICNSProviderInfo() {
super( super(
ICNSProviderInfo.class, ICNSProviderInfo.class,
new String[]{"icns", "ICNS"}, new String[]{"icns", "ICNS"},
@ -50,9 +50,12 @@ final class ICNSProviderInfo extends ReaderWriterProviderInfo {
}, },
"com.twelvemonkeys.imageio.plugins.icns.ICNSImageReader", "com.twelvemonkeys.imageio.plugins.icns.ICNSImageReader",
new String[] {"com.twelvemonkeys.imageio.plugins.icns.ICNSImageReaderSpi"}, new String[] {"com.twelvemonkeys.imageio.plugins.icns.ICNSImageReaderSpi"},
"com.twelvemonkeys.imageio.plugins.icns.ICNSImageWriter",
new String[] {"com.twelvemonkeys.imageio.plugins.icns.ICNSImageWriterSpi"},
false, null, null,
null, null, null, null,
false, null, null, null, null, true, null,
true, null, null, null, null null, null, null
); );
} }
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2011, Harald Kuhr * Copyright (c) 2017, Harald Kuhr
* All rights reserved. * All rights reserved.
* *
* Redistribution and use in source and binary forms, with or without * Redistribution and use in source and binary forms, with or without
@ -34,6 +34,7 @@ import com.twelvemonkeys.lang.Validate;
import javax.imageio.stream.ImageInputStream; import javax.imageio.stream.ImageInputStream;
import java.awt.*; import java.awt.*;
import java.awt.image.RenderedImage;
import java.io.IOException; import java.io.IOException;
/** /**
@ -46,9 +47,9 @@ import java.io.IOException;
final class IconResource { final class IconResource {
// TODO: Rewrite using subclasses/instances! // TODO: Rewrite using subclasses/instances!
protected final long start; final long start;
protected final int type; final int type;
protected final int length; final int length;
private IconResource(long start, int type, int length) { private IconResource(long start, int type, int length) {
validate(type, length); validate(type, length);
@ -58,8 +59,12 @@ final class IconResource {
this.length = length; this.length = length;
} }
public static IconResource read(ImageInputStream input) throws IOException { static IconResource read(final ImageInputStream input) throws IOException {
return new IconResource(input.getStreamPosition(), input.readInt(), input.readInt()); return read(input.getStreamPosition(), input);
}
static IconResource read(final long offset, final ImageInputStream input) throws IOException {
return new IconResource(offset, input.readInt(), input.readInt());
} }
private void validate(int type, int length) { private void validate(int type, int length) {
@ -113,9 +118,17 @@ final class IconResource {
case ICNS.is32: case ICNS.is32:
case ICNS.il32: case ICNS.il32:
case ICNS.it32: case ICNS.it32:
case ICNS.icp4:
case ICNS.icp5:
case ICNS.icp6:
case ICNS.ic07:
case ICNS.ic08: case ICNS.ic08:
case ICNS.ic09: case ICNS.ic09:
case ICNS.ic10: case ICNS.ic10:
case ICNS.ic11:
case ICNS.ic12:
case ICNS.ic13:
case ICNS.ic14:
if (length > ICNS.RESOURCE_HEADER_SIZE) { if (length > ICNS.RESOURCE_HEADER_SIZE) {
break; break;
} }
@ -142,7 +155,7 @@ final class IconResource {
); );
} }
public Dimension size() { Dimension size() {
switch (type) { switch (type) {
case ICNS.ICON: case ICNS.ICON:
case ICNS.ICN_: case ICNS.ICN_:
@ -156,11 +169,14 @@ final class IconResource {
case ICNS.ics8: case ICNS.ics8:
case ICNS.is32: case ICNS.is32:
case ICNS.s8mk: case ICNS.s8mk:
case ICNS.icp4:
return new Dimension(16, 16); return new Dimension(16, 16);
case ICNS.icl4: case ICNS.icl4:
case ICNS.icl8: case ICNS.icl8:
case ICNS.il32: case ICNS.il32:
case ICNS.l8mk: case ICNS.l8mk:
case ICNS.icp5:
case ICNS.ic11:
return new Dimension(32, 32); return new Dimension(32, 32);
case ICNS.ich_: case ICNS.ich_:
case ICNS.ich4: case ICNS.ich4:
@ -168,12 +184,18 @@ final class IconResource {
case ICNS.ih32: case ICNS.ih32:
case ICNS.h8mk: case ICNS.h8mk:
return new Dimension(48, 48); return new Dimension(48, 48);
case ICNS.icp6:
case ICNS.ic12:
return new Dimension(64, 64);
case ICNS.it32: case ICNS.it32:
case ICNS.t8mk: case ICNS.t8mk:
case ICNS.ic07:
return new Dimension(128, 128); return new Dimension(128, 128);
case ICNS.ic08: case ICNS.ic08:
case ICNS.ic13:
return new Dimension(256, 256); return new Dimension(256, 256);
case ICNS.ic09: case ICNS.ic09:
case ICNS.ic14:
return new Dimension(512, 512); return new Dimension(512, 512);
case ICNS.ic10: case ICNS.ic10:
return new Dimension(1024, 1024); return new Dimension(1024, 1024);
@ -182,7 +204,7 @@ final class IconResource {
} }
} }
public int depth() { int depth() {
switch (type) { switch (type) {
case ICNS.ICON: case ICNS.ICON:
case ICNS.ICN_: case ICNS.ICN_:
@ -208,16 +230,24 @@ final class IconResource {
case ICNS.il32: case ICNS.il32:
case ICNS.ih32: case ICNS.ih32:
case ICNS.it32: case ICNS.it32:
case ICNS.icp4:
case ICNS.icp5:
case ICNS.icp6:
case ICNS.ic07:
case ICNS.ic08: case ICNS.ic08:
case ICNS.ic09: case ICNS.ic09:
case ICNS.ic10: case ICNS.ic10:
case ICNS.ic11:
case ICNS.ic12:
case ICNS.ic13:
case ICNS.ic14:
return 32; return 32;
default: default:
throw new IllegalStateException(String.format("Unknown icon type: '%s'", ICNSUtil.intToStr(type))); throw new IllegalStateException(String.format("Unknown icon type: '%s'", ICNSUtil.intToStr(type)));
} }
} }
public boolean isUnknownType() { boolean isUnknownType() {
// Unknown types should simply be skipped when reading // Unknown types should simply be skipped when reading
switch (type) { switch (type) {
case ICNS.ICON: case ICNS.ICON:
@ -241,16 +271,24 @@ final class IconResource {
case ICNS.il32: case ICNS.il32:
case ICNS.ih32: case ICNS.ih32:
case ICNS.it32: case ICNS.it32:
case ICNS.icp4:
case ICNS.icp5:
case ICNS.icp6:
case ICNS.ic07:
case ICNS.ic08: case ICNS.ic08:
case ICNS.ic09: case ICNS.ic09:
case ICNS.ic10: case ICNS.ic10:
case ICNS.ic11:
case ICNS.ic12:
case ICNS.ic13:
case ICNS.ic14:
return false; return false;
} }
return true; return true;
} }
public boolean hasMask() { boolean hasMask() {
switch (type) { switch (type) {
case ICNS.ICN_: case ICNS.ICN_:
case ICNS.icm_: case ICNS.icm_:
@ -262,7 +300,7 @@ final class IconResource {
return false; return false;
} }
public boolean isMaskType() { boolean isMaskType() {
switch (type) { switch (type) {
case ICNS.s8mk: case ICNS.s8mk:
case ICNS.l8mk: case ICNS.l8mk:
@ -274,7 +312,7 @@ final class IconResource {
return false; return false;
} }
public boolean isCompressed() { boolean isCompressed() {
switch (type) { switch (type) {
case ICNS.is32: case ICNS.is32:
case ICNS.il32: case ICNS.il32:
@ -291,18 +329,30 @@ final class IconResource {
return false; return false;
} }
public boolean isForeignFormat() { boolean isForeignFormat() {
// Recent entries contains full JPEG 2000 or PNG streams // Recent entries contains full JPEG 2000 or PNG streams
switch (type) { switch (type) {
case ICNS.icp4:
case ICNS.icp5:
case ICNS.icp6:
case ICNS.ic07:
case ICNS.ic08: case ICNS.ic08:
case ICNS.ic09: case ICNS.ic09:
case ICNS.ic10: case ICNS.ic10:
case ICNS.ic11:
case ICNS.ic12:
case ICNS.ic13:
case ICNS.ic14:
return true; return true;
} }
return false; return false;
} }
boolean isTOC() {
return type == ICNS.TOC_;
}
@Override @Override
public int hashCode() { public int hashCode() {
return (int) start ^ type; return (int) start ^ type;
@ -322,4 +372,62 @@ final class IconResource {
public String toString() { public String toString() {
return String.format("%s['%s' start: %d, length: %d%s]", getClass().getSimpleName(), ICNSUtil.intToStr(type), start, length, isCompressed() ? " (compressed)" : ""); return String.format("%s['%s' start: %d, length: %d%s]", getClass().getSimpleName(), ICNSUtil.intToStr(type), start, length, isCompressed() ? " (compressed)" : "");
} }
static int typeFromImage(final RenderedImage image, final String compression) {
int width = image.getWidth();
int height = image.getHeight();
if (width == height) {
switch (compression) {
case "JPEG2000":
case "PNG":
return typeFromWidthForeign(width);
case "None":
case "RLE":
return typeFromWidthNative(width);
default:
throw new IllegalArgumentException("Unsupported compression for ICNS: " + compression);
}
}
// Note: Strictly, the format supports an ancient 16x12 size, but I doubt we'll ever support that
throw new IllegalArgumentException(String.format("Unsupported dimensions for ICNS, only square icons supported: %dx%d", width, height));
}
// NOTE: These also needs a mask, if there's an alpha channel
private static int typeFromWidthNative(final int width) {
switch (width) {
case 16:
return ICNS.is32;
case 32:
return ICNS.il32;
case 48:
return ICNS.ih32;
case 128:
return ICNS.it32;
default:
throw new IllegalArgumentException(String.format("Unsupported dimensions for ICNS, only 16, 32, 48 and 128 supported: %dx%d", width, width));
}
}
private static int typeFromWidthForeign(final int width) {
switch (width) {
case 16:
return ICNS.icp4;
case 32:
return ICNS.icp5;
case 64:
return ICNS.icp6;
case 128:
return ICNS.ic07;
case 256:
return ICNS.ic08;
case 512:
return ICNS.ic09;
case 1024:
return ICNS.ic10;
default:
throw new IllegalArgumentException(String.format("Unsupported dimensions for ICNS, only multiples of 2 from 16 to 1024 supported: %dx%d", width, width));
}
}
} }

View File

@ -1 +1 @@
com.twelvemonkeys.imageio.plugins.icns.ICNSImageReaderSpi com.twelvemonkeys.imageio.plugins.icns.ICNSImageReaderSpi

View File

@ -0,0 +1 @@
com.twelvemonkeys.imageio.plugins.icns.ICNSImageWriterSpi

View File

@ -0,0 +1,189 @@
/*
* Copyright (c) 2017, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.imageio.plugins.icns;
import com.twelvemonkeys.imageio.util.ImageWriterAbstractTest;
import org.junit.Test;
import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriter;
import javax.imageio.stream.ImageOutputStream;
import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.List;
import static java.util.Arrays.asList;
import static org.junit.Assert.assertTrue;
/**
* ICNSImageWriterTest.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: harald.kuhr$
* @version $Id: ICNSImageWriterTest.java,v 1.0 25/08/2018 harald.kuhr Exp$
*/
public class ICNSImageWriterTest extends ImageWriterAbstractTest {
private final ICNSImageWriterSpi provider = new ICNSImageWriterSpi();
@Override
protected ImageWriter createImageWriter() {
return provider.createWriterInstance(null);
}
@Override
protected List<? extends RenderedImage> getTestData() {
return asList(
new BufferedImage(32, 32, BufferedImage.TYPE_INT_ARGB),
new BufferedImage(32, 32, BufferedImage.TYPE_BYTE_BINARY),
new BufferedImage(32, 32, BufferedImage.TYPE_BYTE_INDEXED),
new BufferedImage(16, 16, BufferedImage.TYPE_INT_ARGB),
// new BufferedImage(48, 48, BufferedImage.TYPE_INT_ARGB), // Only supported for compression None/RLE
new BufferedImage(64, 64, BufferedImage.TYPE_INT_ARGB),
new BufferedImage(128, 128, BufferedImage.TYPE_INT_ARGB),
new BufferedImage(256, 256, BufferedImage.TYPE_INT_ARGB),
new BufferedImage(512, 512, BufferedImage.TYPE_INT_ARGB),
new BufferedImage(1024, 1024, BufferedImage.TYPE_INT_ARGB)
);
}
@Test(expected = IllegalArgumentException.class)
public void testWriteNonSquare() throws IOException {
// ICNS only supports square icons (except some arcane 16x12 we don't currently support)
ImageWriter writer = createImageWriter();
try (ImageOutputStream stream = ImageIO.createImageOutputStream(new ByteArrayOutputStream())) {
writer.setOutput(stream);
writer.write(new BufferedImage(32, 64, BufferedImage.TYPE_INT_ARGB));
}
finally {
writer.dispose();
}
}
@Test(expected = IllegalArgumentException.class)
public void testWriteBadSize() throws IOException {
// ICNS only supports sizes in multiples of 2 (16, 32, 64, ..., 1024 + 48 and 96)
ImageWriter writer = createImageWriter();
try (ImageOutputStream stream = ImageIO.createImageOutputStream(new ByteArrayOutputStream())) {
writer.setOutput(stream);
writer.write(new BufferedImage(17, 17, BufferedImage.TYPE_INT_ARGB));
}
finally {
writer.dispose();
}
}
@Test
public void testSequencesSupported() {
ImageWriter writer = createImageWriter();
try {
assertTrue(writer.canWriteSequence());
}
finally {
writer.dispose();
}
}
@Test(expected = IllegalStateException.class)
public void testWriteSequenceNotStarted() throws IOException {
// ICNS only supports sizes in multiples of 2 (16, 32, 64, ..., 1024 + 48 and 96)
ImageWriter writer = createImageWriter();
try (ImageOutputStream stream = ImageIO.createImageOutputStream(new ByteArrayOutputStream())) {
writer.setOutput(stream);
BufferedImage image = new BufferedImage(32, 32, BufferedImage.TYPE_INT_ARGB);
writer.writeToSequence(new IIOImage(image, null, null), writer.getDefaultWriteParam());
}
finally {
writer.dispose();
}
}
@Test(expected = IllegalStateException.class)
public void testEndSequenceNotStarted() throws IOException {
// ICNS only supports sizes in multiples of 2 (16, 32, 64, ..., 1024 + 48 and 96)
ImageWriter writer = createImageWriter();
try (ImageOutputStream stream = ImageIO.createImageOutputStream(new ByteArrayOutputStream())) {
writer.setOutput(stream);
writer.endWriteSequence();
}
finally {
writer.dispose();
}
}
@Test(expected = IllegalStateException.class)
public void testPrepareSequenceAlreadyStarted() throws IOException {
// ICNS only supports sizes in multiples of 2 (16, 32, 64, ..., 1024 + 48 and 96)
ImageWriter writer = createImageWriter();
try (ImageOutputStream stream = ImageIO.createImageOutputStream(new ByteArrayOutputStream())) {
writer.setOutput(stream);
writer.prepareWriteSequence(null);
writer.prepareWriteSequence(null);
}
finally {
writer.dispose();
}
}
@Test
public void testWriteSequence() throws IOException {
ImageWriter writer = createImageWriter();
ByteArrayOutputStream output = new ByteArrayOutputStream();
try (ImageOutputStream stream = ImageIO.createImageOutputStream(output)) {
writer.setOutput(stream);
writer.prepareWriteSequence(null);
for (RenderedImage image : getTestData()) {
IIOImage iioImage = new IIOImage(image, null, null);
writer.writeToSequence(iioImage, writer.getDefaultWriteParam());
}
writer.endWriteSequence();
}
finally {
writer.dispose();
}
}
}