Skip to content

Commit

Permalink
[GEOT-6016] WMS client may return an opaque image when cascading a tr…
Browse files Browse the repository at this point in the history
…ansparent output from MapXTreme
  • Loading branch information
aaime committed May 16, 2018
1 parent 0832f61 commit b9d1fa9
Show file tree
Hide file tree
Showing 13 changed files with 184 additions and 19 deletions.
Expand Up @@ -25,8 +25,8 @@
import java.net.URL;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageIO;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.image.io.ImageIOExt;
import org.geotools.util.logging.Logging;

/**
Expand Down Expand Up @@ -172,7 +172,7 @@ public BufferedImage getBufferedImage() {
}

public BufferedImage loadImageTileImage(Tile tile) throws IOException {
return ImageIO.read(getUrl());
return ImageIOExt.readBufferedImage(getUrl());
}

/**
Expand Down
Expand Up @@ -22,6 +22,7 @@
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageIO;
import org.geotools.image.io.ImageIOExt;
import org.geotools.tile.ImageLoader;
import org.geotools.tile.Tile;
import org.geotools.util.logging.Logging;
Expand Down Expand Up @@ -67,14 +68,14 @@ public BufferedImage loadImageTileImage(Tile tile) throws IOException {
+ "' at "
+ imgFile.getAbsolutePath());
}
img = ImageIO.read(imgFile);
img = ImageIOExt.readBufferedImage(imgFile);

} else {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine(
"Not found in cache '" + tile.getId() + "'. Loading from " + tile.getUrl());
}
img = ImageIO.read(tile.getUrl());
img = ImageIOExt.readBufferedImage(tile.getUrl());
ImageIO.write(img, "png", imgFile);
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Wrote to cache " + imgFile.getAbsolutePath());
Expand Down
Expand Up @@ -4,7 +4,7 @@
import java.awt.Rectangle;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
Expand All @@ -15,7 +15,6 @@
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageIO;
import org.apache.commons.io.IOUtils;
import org.geotools.coverage.grid.GridCoverage2D;
import org.geotools.coverage.grid.GridCoverageFactory;
Expand All @@ -31,6 +30,7 @@
import org.geotools.geometry.DirectPosition2D;
import org.geotools.geometry.GeneralEnvelope;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.image.io.ImageIOExt;
import org.geotools.ows.ServiceException;
import org.geotools.referencing.CRS;
import org.geotools.renderer.lite.RendererUtilities;
Expand Down Expand Up @@ -275,7 +275,7 @@ GridCoverage2D getMap(
GetMapResponse response = wms.issueRequest(mapRequest);
try {
is = response.getInputStream();
BufferedImage image = ImageIO.read(is);
RenderedImage image = ImageIOExt.read(is);
if (image == null) {
throw new IOException("GetMap failed: " + mapRequest.getFinalURL());
}
Expand Down
Expand Up @@ -26,13 +26,13 @@
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageIO;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.io.IOUtils;
import org.geotools.data.wmts.model.WMTSServiceType;
import org.geotools.image.io.ImageIOExt;
import org.geotools.tile.Tile;
import org.geotools.tile.TileIdentifier;
import org.geotools.tile.TileService;
Expand Down Expand Up @@ -230,7 +230,7 @@ public BufferedImage doLoadImageTileImage(Tile tile) throws IOException {
InputStream is = null;
try {
is = setupInputStream(getUrl(), headers);
return ImageIO.read(is);
return ImageIOExt.readBufferedImage(is);
} finally {
IOUtils.closeQuietly(is);
}
Expand Down
Expand Up @@ -23,11 +23,11 @@
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageIO;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.xml.namespace.QName;
import org.geotools.data.Base64;
import org.geotools.image.io.ImageIOExt;
import org.geotools.se.v1_1.SE;
import org.geotools.util.logging.Logging;
import org.geotools.xml.*;
Expand Down Expand Up @@ -117,7 +117,7 @@ private static Icon parseIcon(String content) {
byte[] bytes = Base64.decode(content);
BufferedImage image = null;
try {
image = ImageIO.read(new ByteArrayInputStream(bytes));
image = ImageIOExt.readBufferedImage(new ByteArrayInputStream(bytes));
} catch (IOException e) {
if (LOGGER.isLoggable(Level.WARNING)) {
LOGGER.log(Level.WARNING, "could not parse graphic inline content: " + content, e);
Expand Down
Expand Up @@ -28,13 +28,13 @@
import java.util.Map;
import java.util.Random;
import java.util.TreeMap;
import javax.imageio.ImageIO;
import javax.measure.unit.Unit;
import org.geotools.coverage.Category;
import org.geotools.coverage.CoverageFactoryFinder;
import org.geotools.coverage.GridSampleDimension;
import org.geotools.factory.GeoTools;
import org.geotools.geometry.GeneralEnvelope;
import org.geotools.image.io.ImageIOExt;
import org.geotools.referencing.CRS;
import org.geotools.referencing.crs.DefaultGeographicCRS;
import org.geotools.resources.i18n.ErrorKeys;
Expand Down Expand Up @@ -352,7 +352,7 @@ public void setBufferedImage(final BufferedImage image) {
* @throws IOException if the image can't be read.
*/
public void setBufferedImage(final File file) throws IOException {
setBufferedImage(ImageIO.read(file));
setBufferedImage(ImageIOExt.readBufferedImage(file));
}

/**
Expand Down
Expand Up @@ -440,7 +440,7 @@ public ImageWorker(RenderingHints hints) {
* @throws IOException if the file can't be read.
*/
public ImageWorker(final File input) throws IOException {
this(ImageIO.read(input));
this(ImageIOExt.read(input));
}

/**
Expand Down
Expand Up @@ -17,22 +17,35 @@
package org.geotools.image.io;

import com.sun.media.imageioimpl.common.PackageUtil;
import java.awt.Color;
import java.awt.image.BufferedImage;
import java.awt.image.ComponentColorModel;
import java.awt.image.RenderedImage;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import javax.imageio.ImageIO;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.spi.IIORegistry;
import javax.imageio.spi.ImageInputStreamSpi;
import javax.imageio.spi.ImageReaderWriterSpi;
import javax.imageio.stream.FileCacheImageOutputStream;
import javax.imageio.stream.ImageInputStream;
import javax.imageio.stream.ImageOutputStream;
import javax.imageio.stream.MemoryCacheImageOutputStream;
import javax.media.jai.PlanarImage;
import org.geotools.image.ImageWorker;
import org.geotools.resources.Classes;
import org.geotools.util.Utilities;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
* Provides an alternative source of image input and output streams that uses optimized behavior.
Expand Down Expand Up @@ -300,4 +313,127 @@ static long computeImageSize(RenderedImage image) {
}
return (long) Math.ceil(bits / 8) * image.getWidth() * image.getHeight();
}

/**
* Reads an image from the given input, working around some JDK reader issues. At the time of
* writing, this applies a work around for PNGs with RGB (no alpha) and a transparent color
* configured in the header, that the JDK reader cannot handle.
*
* @param input A non null image source, like a {@link File}, {@link java.net.URL}, or {@link
* java.io.InputStream}
* @return A image
* @throws IOException
*/
public static RenderedImage read(Object input) throws IOException {
if (input == null) {
throw new IllegalArgumentException("input == null!");
}

// build an image input stream
ImageInputStream stream;
if (input instanceof ImageInputStream) {
stream = (ImageInputStream) input;
} else {
stream = ImageIO.createImageInputStream(input);
}
if (stream == null) {
throw new IOException("Can't create an ImageInputStream!");
}

// get the readers
Iterator iter = ImageIO.getImageReaders(stream);
if (!iter.hasNext()) {
return null;
}

ImageReader reader = (ImageReader) iter.next();
// work around PNG with transparent RGB color if needed
// we can remove it once we run on JDK 11, see
// https://bugs.openjdk.java.net/browse/JDK-6788458
boolean isJdkPNGReader =
"com.sun.imageio.plugins.png.PNGImageReader".equals(reader.getClass().getName());
// if it's the JDK PNG reader, we cannot skip the metadata, the tRNS section will be in
// there
reader.setInput(stream, true, !isJdkPNGReader);

BufferedImage bi;
try {
ImageReadParam param = reader.getDefaultReadParam();
bi = reader.read(0, param);
} finally {
reader.dispose();
stream.close();
}

// apply transparency in post-processing if needs be
if (isJdkPNGReader
&& bi.getColorModel() instanceof ComponentColorModel
&& !bi.getColorModel().hasAlpha()
&& bi.getColorModel().getNumComponents() == 3) {
IIOMetadata imageMetadata = reader.getImageMetadata(0);
Node tree = imageMetadata.getAsTree(imageMetadata.getNativeMetadataFormatName());
Node trns_rgb = getNodeFromPath(tree, Arrays.asList("tRNS", "tRNS_RGB"));
if (trns_rgb != null) {
NamedNodeMap attributes = trns_rgb.getAttributes();
Integer red = getIntegerAttribute(attributes, "red");
Integer green = getIntegerAttribute(attributes, "green");
Integer blue = getIntegerAttribute(attributes, "blue");

if (red != null && green != null && blue != null) {
Color color = new Color(red, green, blue);
ImageWorker iw = new ImageWorker(bi);
iw.makeColorTransparent(color);
return iw.getRenderedImage();
}
}
}

return bi;
}

/**
* Same as {@link #read(Object)} but ensures the result is a {@link BufferedImage}, eventually
* transforming it if needs be. Callers that can deal with {@link RenderedImage} should use the
* other method for efficiency sake.
*
* @return A image
* @throws IOException
*/
public static BufferedImage readBufferedImage(Object input) throws IOException {
RenderedImage ri = ImageIOExt.read(input);
if (ri == null) {
return null;
} else if (ri instanceof BufferedImage) {
return (BufferedImage) ri;
} else {
return PlanarImage.wrapRenderedImage(ri).getAsBufferedImage();
}
}

private static Integer getIntegerAttribute(NamedNodeMap attributes, String attributeName) {
return Optional.ofNullable(attributes.getNamedItem(attributeName))
.map(n -> n.getNodeValue())
.map(s -> Integer.valueOf(s))
.orElse(null);
}

/** Locates a node in the tree, by giving a list of path components */
private static Node getNodeFromPath(Node root, List<String> pathComponents) {
if (pathComponents.isEmpty()) {
return root;
}

String firstComponent = pathComponents.get(0);
NodeList childNodes = root.getChildNodes();
for (int i = 0; i < childNodes.getLength(); i++) {
Node child = childNodes.item(i);

if (firstComponent.equals(child.getNodeName())) {
return getNodeFromPath(child, pathComponents.subList(1, pathComponents.size()));
}
}

// not found
return null;
}
}
Expand Up @@ -16,17 +16,22 @@
*/
package org.geotools.image.io;

import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;

import java.awt.image.ColorModel;
import java.awt.image.ComponentColorModel;
import java.awt.image.RenderedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import javax.imageio.ImageIO;
import javax.imageio.stream.FileCacheImageOutputStream;
import javax.imageio.stream.ImageOutputStream;
import javax.imageio.stream.MemoryCacheImageOutputStream;
import javax.media.jai.operator.ConstantDescriptor;
import org.geotools.image.ImageWorker;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
Expand Down Expand Up @@ -97,4 +102,25 @@ RenderedImage getTestRenderedImage(int width, int height, int bands) {
}
return ConstantDescriptor.create((float) width, (float) height, values, null);
}

@Test
public void testPNGTransparency() throws Exception {
try (InputStream is = ImageWorker.class.getResourceAsStream("test-data/rgb_trns.png")) {
RenderedImage image = ImageIOExt.read(is);

// color model check
ColorModel colorModel = image.getColorModel();
assertThat(colorModel, instanceOf(ComponentColorModel.class));
assertEquals(4, colorModel.getNumComponents());
assertEquals(ColorModel.TRANSLUCENT, colorModel.getTransparency());

// transparency check, the pixel is green but transparent
int[] pixel = new int[4];
image.getData().getPixel(32, 32, pixel);
assertEquals(0, pixel[0]);
assertEquals(255, pixel[1]);
assertEquals(52, pixel[2]);
assertEquals(0, pixel[3]);
}
}
}
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Expand Up @@ -40,10 +40,10 @@
import java.util.Iterator;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageIO;
import javax.swing.Icon;
import org.geotools.geometry.jts.Decimator;
import org.geotools.geometry.jts.LiteShape2;
import org.geotools.image.io.ImageIOExt;
import org.geotools.referencing.operation.transform.AffineTransform2D;
import org.geotools.renderer.style.GraphicStyle2D;
import org.geotools.renderer.style.IconStyle2D;
Expand Down Expand Up @@ -439,7 +439,8 @@ public void paint(
iter.currentSegment(coords);
try {
BufferedImage image =
ImageIO.read(graphic.getOnlineResource().getLinkage().toURL());
ImageIOExt.readBufferedImage(
graphic.getOnlineResource().getLinkage().toURL());
if ((symbolScale > 0.0) && (symbolScale != 1.0)) {
int w = (int) (image.getWidth() / symbolScale);
int h = (int) (image.getHeight() / symbolScale);
Expand Down

0 comments on commit b9d1fa9

Please sign in to comment.