--- old/modules/graphics/src/main/java/com/sun/javafx/iio/bmp/BMPImageLoaderFactory.java 2014-05-30 13:09:43.000000000 +0400 +++ new/modules/graphics/src/main/java/com/sun/javafx/iio/bmp/BMPImageLoaderFactory.java 2014-05-30 13:09:43.000000000 +0400 @@ -84,6 +84,12 @@ static final int BIH_SIZE = 40; static final int BIH4_SIZE = 108; static final int BIH5_SIZE = 124; + static final int BI_RGB = 0; + static final int BI_RLE8 = 1; + static final int BI_RLE4 = 2; + static final int BI_BITFIELDS = 3; + static final int BI_JPEG = 4; + static final int BI_PNG = 5; final int biSize; final int biWidth; @@ -120,11 +126,28 @@ validate(); } - void validate() { - if (biCompression != 0 || biPlanes != 1 || biBitCount != 24) { - throw new RuntimeException( - "Unsupported BMP image: " + - "only 24 bit uncompressed BMP`s is supported"); + void validate() throws IOException { + if (biBitCount < 1 + || biCompression == BI_JPEG || biCompression == BI_PNG) { + throw new IOException( + "Unsupported BMP image: " + + "Embedded JPEG or PNG images are not supported"); + } + + if (biCompression == BI_RLE4 && biBitCount != 4) { + throw new IOException( + "Invalid BMP image: " + + "Only 4 bpp images can be RLE4 compressed"); + } + if (biCompression == BI_RLE8 && biBitCount != 8) { + throw new IOException( + "Invalid BMP image: " + + "Only 8 bpp images can be RLE8 compressed"); + } + if (biCompression == BI_BITFIELDS) { + throw new IOException( + "Unsupported BMP image: " + + "Bitfields BMP files are not supported"); } } } @@ -136,19 +159,20 @@ final LEInputStream data; - short bfType; // must be equal to BM int bfSize; int bfOffBits; - int bgra_palette[]; + byte bgra_palette[]; + int masks[] = new int[3]; + int maskOffsets[] = new int[3]; BitmapInfoHeader bih; BMPImageLoader(InputStream input) throws IOException { super(BMPDescriptor.theInstance); data = new LEInputStream(input); - bfType = data.readShort(); - if (isValid()) { - readHeader(); + if (data.readShort() != BM) { + throw new IOException("Invalid BMP file signature"); } + readHeader(); } private void readHeader() throws IOException { @@ -156,16 +180,222 @@ data.skipBytes(4); // 32 bits reserved bfOffBits = data.readInt(); bih = new BitmapInfoHeader(data); + if (bfOffBits < bih.biSize + BFH_SIZE) { + throw new IOException("Invalid bitmap bits offset"); + } + + // assign default masks + // TODO: parse BI_BITFIELDS + if (bih.biBitCount == 16) { + masks[0] = 0x7C00; + masks[1] = 0x03E0; + masks[2] = 0x001F; + maskOffsets[0] = 10; + maskOffsets[1] = 5; + maskOffsets[2] = 0; + } else if (bih.biBitCount == 32) { + masks[0] = 0x00FF0000; + masks[1] = 0x0000FF00; + masks[2] = 0x000000FF; + maskOffsets[0] = 24; + maskOffsets[1] = 16; + maskOffsets[2] = 0; + } + if (bih.biSize + BFH_SIZE != bfOffBits) { - data.skipBytes(bfOffBits - bih.biSize - BFH_SIZE); + int length = bfOffBits - bih.biSize - BFH_SIZE; + int paletteSize = length / 4; + bgra_palette = new byte[paletteSize * 4]; + int read = data.in.read(bgra_palette); + // goto bitmap bits + if (read < length) { + data.in.skip(length - read); + } + } + } + + @Override + public void dispose() { + } + + + private void readRLE(byte[] image, int rowLength, int hght, boolean isRLE4) + throws IOException + { + int imgSize = bih.biSizeImage; + if (imgSize == 0) { + imgSize = bfSize - bfOffBits; + } + byte imgData[] = new byte[imgSize]; + if (data.in.read(imgData) < imgSize) { + return; + } + + boolean isBottomUp = bih.biHeight > 0; + int line = isBottomUp ? hght - 1 : 0; + int i = 0; + int x = 0; + while (i < imgSize) { + int b1 = getByte(imgData, i++); + int b2 = getByte(imgData, i++); + if (b1 == 0) { // absolute + switch (b2) { + case 0: // end of line + x = 0; + line += isBottomUp ? -1 : 1; + break; + case 1: // end of bitmap + return; + case 2: // delta + int deltaX = getByte(imgData, i++); + int deltaY = getByte(imgData, i++); + x += deltaX; + line += deltaY; + break; + default: + int indexData = 0; + int index; + for (int p = 0; p < b2; p++) { + if (isRLE4) { + if ((p & 1) == 0) { + indexData = getByte(imgData, i++); + index = (indexData & 0xf0) >> 4; + } else { + index = indexData & 0x0f; + } + } else { + index = getByte(imgData, i++); + } + setRGBFromPalette(image, rowLength, x++, line, index); + } + if (isRLE4) { + if ((b2 & 3) == 1 || (b2 & 3) == 2) i++; + } else { + if ((b2 & 1) == 1) i++; + } + break; + } + } else { // encoded + if (isRLE4) { + int index1 = (b2 & 0xf0) >> 4; + int index2 = b2 & 0x0f; + for (int p = 0; p < b1; p++) { + setRGBFromPalette(image, rowLength, x++, line, + (p & 1) == 0 ? index1 : index2); + } + } else { + for (int p = 0; p < b1; p++) { + setRGBFromPalette(image, rowLength, x++, line, b2); + } + } + } + } + + } + + private void setRGBFromPalette(byte[] image, int rowLength, int x, int y, int index) { + int i = y * rowLength + x * 3; + image[i] = bgra_palette[index * 4 + 2]; + image[i + 1] = bgra_palette[index * 4 + 1]; + image[i + 2] = bgra_palette[index * 4]; + } + + private void readPackedBits(byte[] image, int rowLength, int hght) + throws IOException + { + int pixPerByte = 8 / bih.biBitCount; + int bytesPerLine = (bih.biWidth + pixPerByte - 1) / pixPerByte; + int srcStride = (bytesPerLine + 3) & ~3; + int bitMask = (1 << bih.biBitCount) - 1; + + byte lineBuf[] = new byte[srcStride]; + for (int i = 0; i != hght; ++i) { + int line = bih.biHeight < 0 ? i : hght - i - 1; + int nRead = data.in.read(lineBuf); + + for (int x = 0; x != bih.biWidth; x++) { + int bitnum = x * bih.biBitCount; + int element = lineBuf[bitnum / 8]; + int shift = 8 - (bitnum & 7) - bih.biBitCount; + int index = (element >> shift) & bitMask; + setRGBFromPalette(image, rowLength, x, line, index); + } + if (nRead != srcStride) { + break; + } + } + } + + private static int getWord(byte[] buf, int pos) { + return buf[pos] & 0xff | buf[pos + 1] << 8 & 0xff00; + } + + private static int getByte(byte[] buf, int pos) { + return buf[pos] & 0xff; + } + + private void read16Bit(byte[] image, int rowLength, int hght) throws IOException { + int bytesPerLine = bih.biWidth * 2; + int srcStride = (bytesPerLine + 3) & ~3; + byte lineBuf[] = new byte[srcStride]; + for (int i = 0; i != hght; ++i) { + int line = bih.biHeight < 0 ? i : hght - i - 1; + int nRead = data.in.read(lineBuf); + + for (int x = 0; x != bih.biWidth; x++) { + int element = getWord(lineBuf, x * 2); + for (int b = 0; b < 3; b++) { + byte c = (byte) ((element & masks[b]) >>> maskOffsets[b]); + c = (byte) ((double) c / ((1 << 5) - 1) * 255 + 0.5); + image[line * rowLength + x * 3 + b] = c; + } + } + if (nRead != srcStride) { + break; + } } } - private boolean isValid() { - return bfType == BM; + private void read32Bit(byte[] image, int rowLength, int hght) throws IOException { + int bytesPerLine = bih.biWidth * 4; + byte lineBuf[] = new byte[bytesPerLine]; + for (int i = 0; i != hght; ++i) { + int line = bih.biHeight < 0 ? i : hght - i - 1; + int nRead = data.in.read(lineBuf); + + for (int x = 0; x != bih.biWidth; x++) { + int element = lineBuf[x] << 24 + | lineBuf[x + 1] << 16 + | lineBuf[x + 2] << 8 + | lineBuf[x + 3]; + for (int b = 0; b < 3; b++) { + image[line * rowLength + x * 3 + b] + = (byte) ((element & masks[b]) >>> maskOffsets[b]); + } + } + if (nRead != bytesPerLine) { + break; + } + } } - public void dispose() { } + private void read24Bit(byte[] image, int rowLength, int hght) throws IOException { + int bmpStride = (rowLength + 3) & ~3; + + for (int i = 0; i != hght; ++i) { + int line = bih.biHeight < 0 ? i : hght - i - 1; + int nRead = data.in.read(image, line * rowLength, rowLength); + GBRtoRGB(image, line * rowLength, nRead); + + if (nRead != rowLength) { + break; + } + + if (nRead < bmpStride) { + data.skipBytes(bmpStride - nRead); + } + } + } static void GBRtoRGB(byte data[], int pos, int size) { for (int sz = size / 3; sz != 0; --sz) { @@ -177,46 +407,60 @@ public ImageFrame load(int imageIndex, int width, int height, boolean preserveAspectRatio, boolean smooth) throws IOException -{ + { if (0 != imageIndex) { return null; } + int hght = Math.abs(bih.biHeight); + if ((width > 0 && width != bih.biWidth) || - (height > 0 && height != bih.biHeight)) + (height > 0 && height != hght)) { - throw new RuntimeException("scaling for BMP is not supported"); + throw new IOException("scaling for BMP is not supported"); } // Pass image metadata to any listeners. ImageMetadata imageMetadata = new ImageMetadata(null, Boolean.TRUE, - null, null, null, null, bih.biWidth, bih.biHeight, + null, null, null, null, bih.biWidth, hght, null, null, null); updateImageMetadata(imageMetadata); - int bmpStride = (bih.biBitCount*bih.biWidth/8 + 3) & ~3; - int rowLength = (bih.biBitCount/8)*bih.biWidth; - - int hght = Math.abs(bih.biHeight); - - byte image[] = new byte[rowLength * hght]; + int stride = bih.biWidth * 3; - for (int i = 0; i != hght; ++i) { - int line = bih.biHeight < 0 ? i : hght-i-1; - int nRead = data.in.read(image, line * rowLength, rowLength); - GBRtoRGB(image, line * rowLength, nRead); + byte image[] = new byte[stride * hght]; - if (nRead != rowLength) { + switch (bih.biBitCount) { + case 1: + readPackedBits(image, stride, hght); + break; + case 4: + if (bih.biCompression == BitmapInfoHeader.BI_RLE4) { + readRLE(image, stride, hght, true); + } else { + readPackedBits(image, stride, hght); + } + break; + case 8: + if (bih.biCompression == BitmapInfoHeader.BI_RLE8) { + readRLE(image, stride, hght, false); + } else { + readPackedBits(image, stride, hght); + } + break; + case 16: + read16Bit(image, stride, hght); + break; + case 32: + read32Bit(image, stride, hght); + break; + case 24: + read24Bit(image, stride, hght); break; - } - - if (nRead < bmpStride) { - data.skipBytes(bmpStride-nRead); - } } return new ImageFrame(ImageStorage.ImageType.RGB, ByteBuffer.wrap(image), - bih.biWidth, hght, rowLength, null, null); + bih.biWidth, hght, stride, null, null); } } @@ -237,4 +481,3 @@ return new BMPImageLoader(input); } } - --- /dev/null 2014-05-30 13:09:44.000000000 +0400 +++ new/modules/graphics/src/test/java/com/sun/javafx/iio/BMPImageLoaderTest.java 2014-05-30 13:09:43.000000000 +0400 @@ -0,0 +1,300 @@ +/* + * Copyright (c) 2014, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.sun.javafx.iio; + +import com.sun.javafx.iio.bmp.BMPImageLoaderFactory; +import com.sun.prism.Image; +import java.awt.Color; +import java.awt.GradientPaint; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import java.awt.image.DataBuffer; +import java.awt.image.IndexColorModel; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Iterator; +import java.util.Random; +import javax.imageio.IIOImage; +import javax.imageio.ImageIO; +import javax.imageio.ImageWriteParam; +import javax.imageio.ImageWriter; +import javax.imageio.stream.ImageOutputStream; +import javax.imageio.stream.MemoryCacheImageInputStream; +import static org.junit.Assert.*; +import org.junit.Test; + +public class BMPImageLoaderTest { + static final boolean writeFiles = false; + static final int testWidth = 509, testHeight = 157; + + ByteArrayInputStream constructStream(int[] bytes) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + for (int b : bytes) { + baos.write(b); + } + return new ByteArrayInputStream(baos.toByteArray()); + } + + void compare(Image img, BufferedImage bImg) { + assertNotNull(img); + assertNotNull(bImg); + int w = bImg.getWidth(), h = bImg.getHeight(); + assertEquals("Unmatched width", w, img.getWidth()); + assertEquals("Unmatched height", h, img.getHeight()); + + for (int y = 0; y < h; y++) { + for (int x = 0; x < w; x++) { + int p1 = bImg.getRGB(x, y); + int p2 = img.getArgb(x, y); + if (p1 != p2) { + throw new org.junit.ComparisonFailure( + "pixel " + x + ", " + y + " does not match", + String.format("0x%08X", p1), String.format("0x%08X", p2) + ); + } + } + } + } + + Image loadImage(InputStream stream) { + ImageLoaderFactory loaderFactory = BMPImageLoaderFactory.getInstance(); + ImageLoader loader = null; + try { + loader = loaderFactory.createImageLoader(stream); + } catch (IOException ioEx) { + fail("unexpected IOException: " + ioEx); + } + assertNotNull(loader); + + try { + ImageFrame frame = loader.load(0, 0, 0, true, true); + return Image.convertImageFrame(frame); + } catch (IOException e) { + fail("unexpected IOException: " + e); + } + return null; + } + + BufferedImage create4BitImage() { + int[] cmap = new int[16]; + int i = 0; + for (int r = 0; r < 2; r++) { + for (int g = 0; g < 2; g++) { + for (int b = 0; b < 2; b++) { + cmap[i++] = 0xff << 24 | r * 255 << 16 | g * 255 << 8 | b * 255; + if ((r | g | b) == 0) { + cmap[i++] = 0xffc0c0c0; + } else { + cmap[i++] = 0xff << 24 | r * 128 << 16 | g * 128 << 8 | b * 128; + } + } + } + } + IndexColorModel cm = new IndexColorModel(4, 16, cmap, 0, false, -1, DataBuffer.TYPE_BYTE); + return new BufferedImage(testWidth, testHeight, BufferedImage.TYPE_BYTE_BINARY, cm); + } + + BufferedImage createImage(int type) { + return new BufferedImage(testWidth, testHeight, type); + } + + void writeImage(BufferedImage bImg, Object out, String compression) { + try { + ImageOutputStream ios = ImageIO.createImageOutputStream(out); + Iterator iter = ImageIO.getImageWritersByFormatName("bmp"); + ImageWriter writer = iter.next(); + ImageWriteParam iwp = writer.getDefaultWriteParam(); + if (compression != null) { + iwp.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); + iwp.setCompressionType(compression); + } + writer.setOutput(ios); + writer.write(null, new IIOImage(bImg, null, null), iwp); + } catch (IOException e) { + fail("unexpected IOException: " + e); + } + } + + Image getImage(BufferedImage bImg, String compression) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + writeImage(bImg, out, compression); + return loadImage(new ByteArrayInputStream(out.toByteArray())); + } + + void testImageType(int type, String fileName, String compression) { + BufferedImage bImg = createImage(type); + testImage(bImg, fileName, compression); + } + + void testImageType(int type, String fileName) { + BufferedImage bImg = createImage(type); + testImage(bImg, fileName, null); + } + + void drawImageGradient(BufferedImage bImg) { + Graphics2D graphics = bImg.createGraphics(); + GradientPaint g = new GradientPaint(0, 0, Color.RED, testWidth, testHeight, Color.GREEN); + graphics.setPaint(g); + graphics.fillRect(0, 0, testWidth, testHeight); + } + + void drawImageRandom(BufferedImage bImg) { + int h = bImg.getHeight(), w = bImg.getWidth(); + Random r = new Random(1); + for (int y = 0; y < h; y++) { + for (int x = 0; x < w; x++) { + bImg.setRGB(x, y, r.nextInt(1 << 24)); + } + } + } + + void drawImageHue(BufferedImage bImg) { + int h = bImg.getHeight(), w = bImg.getWidth(); + for (int y = 0; y < h; y++) { + float s = 2.0f * y / h; + if (s > 1) { + s = 1; + } + float b = 2.0f * (h - y) / h; + if (b > 1) { + b = 1; + } + for (int x = 0; x < w; x++) { + float hue = (float) x / w; + bImg.setRGB(x, y, Color.HSBtoRGB(hue, s, b)); + } + } + } + + void drawImageAll(BufferedImage bImg) { + int h = bImg.getHeight(), w = bImg.getWidth(); + //if (h*w < (1<<24)) return; + for (int y = 0; y < h; y++) { + for (int x = 0; x < w; x++) { + bImg.setRGB(x, y, y * h + x); + } + } + } + + void testImage(BufferedImage bImg, String fileName, String compression) { + //drawImageHue(bImg); + //drawImageAll(bImg); + drawImageRandom(bImg); + if (writeFiles && fileName != null) { + File file = new File(fileName); + file.delete(); + writeImage(bImg, file, compression); + } + Image image = getImage(bImg, compression); + compare(image, bImg); + } + + @Test + public void testRT32213() { + final int[] bytes = { + 0x42, 0x4d, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x28, 0x00, + 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, + 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xff, 0xff, 0xff, 0x00, 0x80, 0x00, + 0x00, 0x00 + }; + + ByteArrayInputStream stream = constructStream(bytes); + Image image = loadImage(stream); + stream.reset(); + try { + BufferedImage bImg = ImageIO.read(new MemoryCacheImageInputStream(stream)); + compare(image, bImg); + } catch (IOException e) { + fail("unexpected IOException: " + e); + } + } + + @Test + public void test1Bit() { + testImageType(BufferedImage.TYPE_BYTE_BINARY, "out1bit.bmp"); + } + + @Test + public void test4Bit() { + testImage(create4BitImage(), "out4bit.bmp", null); + } + + //@Test + public void test4BitRLE() { + testImage(create4BitImage(), "out4bitRLE.bmp", "BI_RLE4"); + } + + @Test + public void test8Bit() { + testImageType(BufferedImage.TYPE_BYTE_INDEXED, "out8bit.bmp"); + } + + @Test + public void test8BitRLE() { + testImageType(BufferedImage.TYPE_BYTE_INDEXED, "out8bitRLE.bmp", "BI_RLE8"); + } + + @Test + public void test16Bit() { + testImageType(BufferedImage.TYPE_USHORT_555_RGB, "out16bit.bmp"); + } + + @Test + public void test24Bit() { + testImageType(BufferedImage.TYPE_INT_RGB, "out24bit.bmp"); + } + + void testFile(String fileName, String outFileName, String compression) { + try { + Image image = loadImage(new FileInputStream(fileName)); + BufferedImage bImg = ImageIO.read(new File(fileName)); + if (writeFiles && outFileName != null) { + File outFile = new File(outFileName); + outFile.delete(); + writeImage(bImg, outFile, compression); + } + compare(image, bImg); + } catch (IOException e) { + fail("unexpected IOException: " + e); + } + } + + //@Test + public void testFiles() { + testFile("pal4rle.bmp", "pal4rleOut.bmp", "BI_RLE4"); + testFile("out4bitRLE.bmp", "out4bitRLEOut.bmp", "BI_RLE4"); + testFile("pal8rletrns.bmp", "pal8rletrnsOut.bmp", null); + } +}