The Exception occurs when reading a multi-page JB2 file where the
first page is of a smaller pixel count (width * height = pixel count)
than the subsequent page(s).
Please see attached JB2 file example for a file that meets the above
condition (testfile.jb2).
Here is sample code that will throw the error (on page 4 of the JB2
file):
final
Iterator<ImageReader> imageReaderIterator = ImageIO
.getImageReadersByFormatName("JBIG2");
final ImageReader
imageReader = imageReaderIterator.hasNext() ? imageReaderIterator
.next() :
null;
imageReader.setInput(ImageIO
.createImageInputStream(new
BufferedInputStream(
new FileInputStream(new File("testfile.jb2")))));
for (final
Iterator<IIOImage> i = imageReader.readAll(null); i
.hasNext();)
{
System.out.println(i.next());
}
Here is a stack trace of that Exception:
java.awt.image.RasterFormatException: Data array too small (should be
12886509 )
at
sun.awt.image.ByteComponentRaster.verify(ByteComponentRaster.java:877)
at
sun.awt.image.ByteComponentRaster.<init>(ByteComponentRaster.java:184)
at
sun.awt.image.ByteInterleavedRaster.<init>(ByteInterleavedRaster.java:
174)
at
sun.awt.image.ByteInterleavedRaster.<init>(ByteInterleavedRaster.java:
96)
at
java.awt.image.Raster.createInterleavedRaster(Raster.java:645)
at com.levigo.jbig2.Bitmap.createRaster(Bitmap.java:
529)
at
com.levigo.jbig2.Bitmap.createBufferedImage(Bitmap.java:567)
at
com.levigo.jbig2.Bitmap.getBufferedImage(Bitmap.java:563)
at
com.levigo.jbig2.JBIG2ImageReader.createGrayScaleImage(JBIG2ImageReader.java:
238)
at
com.levigo.jbig2.JBIG2ImageReader.read(JBIG2ImageReader.java:216)
at javax.imageio.ImageReader.readAll(ImageReader.java:
1169)
The issue is in the class com.levigo.jbig2.JBIG2ImageReader.
We have the following method from that class:
@Override
public ImageReadParam getDefaultReadParam() {
int width = 1;
int height = 1;
try {
width = getWidth(0);
height = getHeight(0);
} catch (IOException e) {
if (log.isInfoEnabled()) {
log.info("Dimensions could not be determined. Returning read
params with size 1x1");
}
}
return new JBIG2ReadParam(1, 1, 0, 0, new Rectangle(0, 0, width,
height), new Dimension(width, height));
}
We see that the width & height values are computed from the width &
height of the first page.
width = getWidth(0);
height = getHeight(0);
This does not work when the subsequent pages are larger than the
first.
Listed below is my fixed and commented JBIG2ImageReader.java. See also
attached.
Thanks,
Chris Laws
ch...@chrislaws.com
Tucson, Arizona, USA
##############################################################################
/**
* Copyright (C) 1995-2010 levigo holding gmbh.
*
* This program is free software: you can redistribute it and/or
modify it under the terms of the
* GNU General Public License as published by the Free Software
Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program 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 for more details.
*
* You should have received a copy of the GNU General Public License
along with this program. If
* not, see <http://www.gnu.org/licenses/>.
*/
package com.levigo.jbig2;
import java.awt.Dimension;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import java.awt.image.Raster;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream;
import com.levigo.jbig2.util.JBIG2Exception;
import com.levigo.jbig2.util.cache.CacheFactory;
import com.levigo.jbig2.util.log.Logger;
import com.levigo.jbig2.util.log.LoggerFactory;
/**
* @see ImageReader
*
* @author <a href="mailto:m.krz...@levigo.de">Matthäus Krzikalla</
a>
*/
public class JBIG2ImageReader extends ImageReader {
private static final Logger log = LoggerFactory
.getLogger(JBIG2ImageReader.class);
public static final boolean DEBUG = false;
public static final boolean PERFORMANCE_TEST = false;
/** JBIG2 document to which we delegate current work.
*/
private JBIG2Document document;
/**
* {@code true} if the source is embedded or {@code
false} if it is a native
* jbig2 file.
*/
private boolean isEmbedded;
/** Globals are JBIG2 segments for PDF wide use. */
private JBIG2Globals globals;
/** ID string in file header, see ISO/IEC 14492:2001,
D.4.1 */
private int[] FILE_HEADER_ID = { 0x97, 0x4A, 0x42,
0x32, 0x0D, 0x0A, 0x1A,
0x0A };
/**
* @see ImageReader#ImageReader(ImageReaderSpi)
*/
protected JBIG2ImageReader(ImageReaderSpi
originatingProvider)
throws IOException {
super(originatingProvider);
}
/**
* @see ImageReader#ImageReader(ImageReaderSpi)
*
* @param originatingProvider
* - The {@code ImageReaderSpi} that is
invoking this
* constructor, or {@code null}.
* @param isEmbedded
* - Flag for embedded data. {@code true} if
data is embedded,
* {@code false} if data is standalone.
* @throws IOException
*/
public JBIG2ImageReader(ImageReaderSpi
originatingProvider,
boolean isEmbedded)
throws IOException {
super(originatingProvider);
this.isEmbedded = isEmbedded;
}
/**
* @see ImageReader#getDefaultReadParam()
*/
@Override
public ImageReadParam getDefaultReadParam() {
// CJL- // int width = 1;
// CJL- // int height = 1;
// CJL- // try {
// CJL- // width = getWidth(0);
// CJL- // / height = getHeight(0);
// CJL- // } catch (IOException e) {
// CJL- // if (log.isInfoEnabled()) {
// CJL- //
// log.info("Dimensions could not be
determined. Returning read params with size 1x1");
// CJL- // }
// CJL- // }
// CJL- // return new
JBIG2ReadParam(1, 1, 0, 0, new Rectangle(0, 0,
// width,
// CJL- // height), new
Dimension(width, height));
return getDefaultReadParam(0); // CJL
+ //
}
/**
* @param imageIndex
* - The image index. In this case it is the
page number.
* @throws JBIG2Exception
*
* @see CJL - THIS IS THE MODIFICATION
*/
// CJL + //
private ImageReadParam getDefaultReadParam(final int
imageIndex) {
int width = 1;
int height = 1;
try {
final int index =
(imageIndex < getDocument().getAmountOfPages()) ? imageIndex
:
0;
width =
getWidth(index);
height =
getHeight(index);
} catch (IOException e) {
if
(log.isInfoEnabled()) {
log.info("Dimensions could not be determined. Returning read params
with size 1x1");
}
}
return new JBIG2ReadParam(1, 1, 0, 0,
new
Rectangle(0, 0, width, height),
new
Dimension(width, height));
}
// CJL + //
/**
* Calculates the width of the specified page.
*
* @param imageIndex
* - The image index. In this case it is the
page number.
*
* @return The width of the specified page.
*
* @throws IOException
* if an error occurs reading the width
information from the
* input source.
*/
@Override
public int getWidth(int imageIndex) throws IOException
{
return
getDocument().getPage(imageIndex + 1).getWidth();
}
/**
* Calculates the height of the specified page.
*
* @param imageIndex
* - The image index. In this case it is the
page number.
*
* @return The height of the specified page or {@code
0} if an error
* occurred.
*
* @throws IOException
* if an error occurs reading the height
information from the
* input source.
*/
@Override
public int getHeight(int imageIndex) throws
IOException {
try {
return
getDocument().getPage(imageIndex + 1).getHeight();
} catch (JBIG2Exception e) {
throw new
IOException(e.getMessage());
}
}
/**
* Simply returns the {@link JBIG2ImageMetadata}.
*
* @return The associated {@link JBIG2ImageMetadata}.
*
* @throws IOException
* if an error occurs reading the height
information from the
* input source.
*/
@Override
public IIOMetadata getImageMetadata(int imageIndex)
throws IOException {
return new
JBIG2ImageMetadata(getDocument().getPage(imageIndex + 1));
}
/**
* Returns the iterator for available image types.
*
* @param imageIndex
* - The page number.
*
* @return An {@link Iterator} for available image
types.
*
* @throws IOException
* if an error occurs reading the height
information from the
* input source.
*/
@Override
public Iterator<ImageTypeSpecifier> getImageTypes(int
imageIndex)
throws IOException {
List<ImageTypeSpecifier> l = new
ArrayList<ImageTypeSpecifier>();
l.add(ImageTypeSpecifier
.createFromBufferedImageType(BufferedImage.TYPE_BYTE_INDEXED));
return l.iterator();
}
/**
* @see ImageReader#getNumImages(boolean)
*/
@Override
public int getNumImages(boolean allowSearch) throws
IOException {
if (allowSearch) {
if
(getDocument().isAmountOfPagesUnknown()) {
log.info("Amount of pages is unknown.");
} else {
return
getDocument().getAmountOfPages();
}
} else {
log.info("Search is
not allowed.");
}
return -1;
}
/**
* This ImageIO plugin doesn't record {@link
IIOMetadata}.
*
* @return {@code null} at every call.
*/
@Override
public IIOMetadata getStreamMetadata() {
log.info("No metadata recorded");
return null;
}
/**
* Returns decoded segments that has been set as
globals. Globals are jbig2
* segments that are used in embedded case for file
wide access. They are
* not assigned to a specific page.
*
* @return Decoded global segments.
*
* @throws IOException
* if an error occurs reading the height
information from the
* input source.
*/
public JBIG2Globals getGlobals() throws IOException {
return
getDocument().getGlobalSegments();
}
/**
* Returns the decoded image of specified page
considering the given
* {@link JBIG2ReadParam}s.
*
* @see ImageReader#read(int, ImageReadParam)
*/
@Override
public BufferedImage read(int imageIndex,
ImageReadParam param)
throws IOException {
try {
return
createGrayScaleImage(imageIndex,
param instanceof JBIG2ReadParam ? (JBIG2ReadParam) param
:
null);
} catch (JBIG2Exception e) {
throw new
IOException(e.getMessage());
}
}
private BufferedImage createGrayScaleImage(int
imageIndex,
JBIG2ReadParam param)
throws JBIG2Exception, IOException {
if (param == null) {
log.info("JBIG2ReadParam not specified. Default will be used.");
// CJL- // param =
(JBIG2ReadParam) getDefaultReadParam();
param =
(JBIG2ReadParam) getDefaultReadParam(imageIndex);// CJL+ //
}
isFileHeaderPresent();
JBIG2Page page =
getDocument().getPage(imageIndex + 1);
if (page == null) { // CJL+ //
throw new
IndexOutOfBoundsException();// CJL+ //
}// CJL+ //
Bitmap pageBitmap = (Bitmap)
CacheFactory.getCache().get(page);
if (pageBitmap != null) {
return
pageBitmap.getBufferedImage(param);
}
pageBitmap = page.getBitmap();
BufferedImage bi =
pageBitmap.getBufferedImage(param);
CacheFactory.getCache().put(page,
pageBitmap,
pageBitmap.getWidth() * pageBitmap.getHeight());
page.clearPageData();
return bi;
}
public boolean canReadRaster() {
return true;
}
@Override
public Raster readRaster(int imageIndex,
ImageReadParam param)
throws IOException {
if (param == null) {
log.info("JBIG2ReadParam not specified. Default will be used.");
// CJL- // param =
(JBIG2ReadParam) getDefaultReadParam();
param =
(JBIG2ReadParam) getDefaultReadParam(imageIndex);// CJL+ //
}
JBIG2Page page =
getDocument().getPage(imageIndex + 1);
if (page == null) { // CJL+ //
throw new
IndexOutOfBoundsException();// CJL+ //
}// CJL+ //
Bitmap pageBitmap = (Bitmap)
CacheFactory.getCache().get(page);
if (pageBitmap != null) {
return
pageBitmap.getRaster((JBIG2ReadParam) param);
}
try {
pageBitmap =
page.getBitmap();
} catch (JBIG2Exception e) {
throw new
IOException(e.getMessage());
}
Raster raster =
pageBitmap.getRaster((JBIG2ReadParam) param);
CacheFactory.getCache().put(page,
pageBitmap,
pageBitmap.getWidth() * pageBitmap.getHeight());
page.clearPageData();
return raster;
}
/**
* Decodes and returns the global segments.
*
* @param globalsInputStream
* - The input stream of globals data.
*
* @return The decoded {@link JBIG2Globals}.
*
* @throws IOException
* if an error occurs reading the height
information from the
* input source.
*/
public JBIG2Globals processGlobals(ImageInputStream
globalsInputStream)
throws IOException {
JBIG2Document doc = new
JBIG2Document(globalsInputStream, true);
return doc.getGlobalSegments();
}
/**
* Simply sets the globals.
*
* @param globals
* - The globals to set.
* @throws IOException
*/
public void setGlobals(JBIG2Globals globals) throws
IOException {
this.globals = globals;
this.document = null;
}
/**
* @see ImageReader#setInput(Object, boolean, boolean)
*/
@Override
public void setInput(Object input, boolean
seekForwardOnly,
boolean
ignoreMetadata) {
super.setInput(input, seekForwardOnly,
ignoreMetadata);
document = null;
isEmbedded = false;
}
private JBIG2Document getDocument() throws IOException
{
if (this.document == null) {
if (this.input ==
null) {
throw
new IOException("Input not set.");
}
if (this.globals ==
null) {
log.info("Globals not set.");
}
this.document = new
JBIG2Document((ImageInputStream) this.input,
isFileHeaderPresent(), this.globals);
}
return this.document;
}
private boolean isFileHeaderPresent() throws
IOException {
if (isEmbedded)
return true;
ImageInputStream underTest =
(ImageInputStream) input;
underTest.mark();
for (int magicByte : FILE_HEADER_ID) {
if (magicByte !=
underTest.read()) {
underTest.reset();
return
true;
}
}
return false;
}
}