Skip to content

Instantly share code, notes, and snippets.

@nimms
Created September 26, 2010 03:38
Show Gist options
  • Save nimms/597574 to your computer and use it in GitHub Desktop.
Save nimms/597574 to your computer and use it in GitHub Desktop.
package au.com.sdx.plan_processing.service.impl;
/**
* imports and logging statements elided
*/
@SuppressWarnings("restriction")
public class TiffProcessor implements IPlanImageService {
private static final int BLACK = 0;
private static final int WHITE = 1;
private static final int LINE_LENGTH = 30; // number of pixels in a row to
private static final int NUM_POTENTIAL_WATERMARKS = 15;
private final File imageFile;
/**
* Takes in an Tiff image from our plan archive and applies a number of image processing steps to it
* and then bundles it into a pdf
* @return The location of the processed pdf
*/
@Override
public String processPlanFile() throws OperationNotSupportedException, IOException, WrappedException {
//vars to keep track of doc info
int numPages = 0;
float width = 0;
float height = 0;
boolean resetDimensions = false;
List<BufferedImage> images = loadAllImagesInTiff(imageFile.getAbsolutePath());
File workDirectory = new File(imageFile.getParent());
for(BufferedImage image : images) {
if(needsInversion(image))
invertImage(image);
removeDots(image);
if(image.getWidth() > image.getHeight()) {
image = rotateImage90DegreesCW(image);
resetDimensions = true;
}
String imageName = workDirectory.getAbsolutePath() + "/" + ++numPages + ".tiff";
writeImage(image, imageName); //writes the image to a working dir since we can't reuse the stream to write directly to the pdf output
}
PdfProcessor service = new PdfProcessor();
String pdfPath = service.createPdf(workDirectory, "output.pdf", width, height);
return pdfPath;
}
/**
* A tiff is essentially a directory structure of compressed images.
* Load all images in a tiff and return them as a list of buffered images
* @param fileName
* @return
* @throws IOException
*/
public List<BufferedImage> loadAllImagesInTiff(String fileName) throws IOException, OperationNotSupportedException {
Iterator<ImageReader> readers = ImageIO.getImageReadersByFormatName("tiff");
TIFFImageReader reader = (TIFFImageReader) readers.next();
File tiff = new File(fileName);
ImageInputStream iis = ImageIO.createImageInputStream(tiff);
reader.setInput(iis, false);
int numImages = reader.getNumImages(true);
List<BufferedImage> images = new ArrayList<BufferedImage>();
for(int i = 0; i < numImages; i ++) {
TIFFImageMetadata metaData = (TIFFImageMetadata) reader.getImageMetadata(i);
TIFFIFD rootIFD = metaData.getRootIFD();
TIFFField field = rootIFD.getTIFFField(262);
// we are only working with a few types of tiffs.
//Any other photometric interpretation is probably a corrupt image in our archive,
//so an exception is thrown to be caught and logged elsewhere.
if(field.getAsInt(0) >= 2)
throw new OperationNotSupportedException("Unsupported Photometric Interpretation: " + field.getAsInt(0));
images.add(reader.read(i));
}
iis.close();
return images;
}
/**
* Manual check to see what photometric interpretation a tiff is using. Given we can't
* trust the validity of the tiff data in our archive,
* we will take a count of the number of ones and zeros and test
* for ourselves.
*
* The two photometric interpretations we handle are MIN_IS_WHITE and MIN_IS_BLACK. in MIN_IS_BLACK,
* we should see far more zeros than ones since black is represented by a zero. If this is the case,
* we will later invert the image and then save it as a MIN_IS_WHITE
* @param image
* @return whether or not we should invert the tiff
*/
public boolean needsInversion(BufferedImage image) {
WritableRaster raster = image.getRaster();
int zeros = 0;
int ones = 0;
for(int y = 0; y < raster.getHeight(); y++) {
for(int x = 0; x < raster.getWidth(); x++) {
int pixelValue = raster.getSample(x, y, 0);
if(pixelValue == 0)
zeros ++;
else
ones++;
}
}
if(ones < zeros)
return true;
else
return false;
}
/**
* removes dust specks from an image
*
* @param image
*/
public void removeDots(BufferedImage image) {
WritableRaster raster = image.getRaster();
int height = raster.getHeight();
int width = raster.getWidth();
for (int y = 0; y <= height; y++) {
for (int x = 0; x <= width; x++) {
int pixelValue = raster.getSample(x, y, 0);
if ((pixelValue == BLACK) && isDot(x, y, raster)) {
raster.setSample(x, y, 0, WHITE);
}
}
}
image.setData(raster);
}
/**
* Rotates an image 90 degrees clockwise. No need to implement counter clockwise at this point
* due to the nature of our image archive
*/
public BufferedImage rotateImage90DegreesCW(BufferedImage image) {
int width = image.getWidth();
int height = image.getHeight();
BufferedImage rotatedImage = new BufferedImage(height, width,
image.getType());
for (int i = 0; i < width; i++)
for (int j = 0; j < height; j++)
rotatedImage.setRGB(j, width - 1 - i, image.getRGB(i, j));
return rotatedImage;
}
/**
* given a width or height that we wish to operate on the middle portion of,
* this function determines the starting point. So if we wish to operate on
* the middle 80% of a 1000px image, the starting point will be at 100px and
* the ending point will be at 900px
*
* @param dimension
* @param percentage
* @return
*/
private int startingPoint(int dimension, double percentage) {
int numPixels = ((int) (dimension * percentage));
return (dimension - numPixels) / 2;
}
/**
* given a width or height that we wish to operate on the middle portion of,
* this function determines the starting point. So if we wish to operate on
* the middle 80% of a 1000px image, the starting point will be at 100px and
* the ending point will be at 900px
*
* @param dimension
* @param percentage
* @return
*/
private int finishingPoint(int dimension, double percentage) {
int numPixels = ((int) (dimension * percentage));
int start = startingPoint(dimension, percentage);
return numPixels + start;
}
/**
* Uses a simple line detection algorithm to identify possible
* watermarks in an image
*
* @param image
* @return
*/
public boolean isWatermarked(BufferedImage image) {
int potentialWaterMarks = 0;
WritableRaster raster = image.getRaster();
int height = raster.getHeight();
int width = raster.getWidth();
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int pixelValue = raster.getSample(x, y, 0);
if (pixelValue == BLACK)
if (isDottedLine(x, y, raster))
potentialWaterMarks++;
}
}
if (potentialWaterMarks > NUM_POTENTIAL_WATERMARKS) {
return true;
} else {
return false;
}
}
/**
* check all the pixels in a 1 pixel proximity of the pixel to see if any of
* them are black
*
* @param x
* @param y
* @param raster
* @return
*/
private boolean isDot(int x, int y, Raster raster) {
for (int height = y - 1; height <= y + 1; height++) {
for (int width = (x - 1); width <= (x + 1); width++) {
if ((width == x) && (height == y)) {
if (raster.getSample(width, height, 0) == WHITE)
return false;
} else {
if (raster.getSample(width, height, 0) == BLACK)
return false;
}
}
}
return true;
}
/**
* From a black pixel, move forward in jumps of 2 px's checking to see if we're looking
* at a dotted line
*/
private boolean isDottedLine(int x, int y, Raster raster) {
for (int i = x + 2; i < x + LINE_LENGTH; i += 2) {
if (!isDot(i, y, raster)) {
return false;
}
}
return true;
}
/**
* For each black pixel, set it to white otherwise set it to black
*
* @param raster
* @return
*/
public void invertImage(BufferedImage image) {
WritableRaster raster = image.getRaster();
for (int y = 0; y < raster.getHeight(); y++) {
for (int x = 0; x < raster.getWidth(); x++) {
if (raster.getSample(x, y, 0) == 0)
raster.setSample(x, y, 0, 1);
else
raster.setSample(x, y, 0, 0);
}
}
image.setData(raster);
}
/**
* Writes a buffered image to an output file. Note that this will overwrite
* a file if it already exists
*
* @param image
* @param outputFile
* @throws IOException
*/
public void writeImage(BufferedImage image, String outputFile)
throws IOException {
Iterator<ImageWriter> writers = ImageIO
.getImageWritersByFormatName("tiff");
ImageWriter writer = writers.next();
TIFFImageWriteParam param = (TIFFImageWriteParam) writer
.getDefaultWriteParam();
param.setTilingMode(TIFFImageWriteParam.MODE_DISABLED);
param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
param.setCompressionType("CCITT T.6");
TIFFImageMetadata metadata = getMetadata(writer, image, param);
File f = new File(outputFile);
if (f.exists()) {
f.delete();
}
ImageOutputStream ios = ImageIO.createImageOutputStream(f);
writer.setOutput(ios);
writer.write(null, new IIOImage(image, null, metadata), param);
ios.close();
}
/**
* The meta data we use for writing tiffs
*/
private TIFFImageMetadata getMetadata(ImageWriter imageWriter,
BufferedImage image, ImageWriteParam param) {
TIFFImageMetadata tiffMetadata = (TIFFImageMetadata) getIIOMetadata(
image, imageWriter, param);
TIFFIFD rootIFD = tiffMetadata.getRootIFD();
BaselineTIFFTagSet base = BaselineTIFFTagSet.getInstance();
// PhotometricInterpretation
rootIFD.addTIFFField(new TIFFField(base.getTag(262), 0));
// Compression
rootIFD.addTIFFField(new TIFFField(base.getTag(259), 4));
// ResolutionUnit
rootIFD.addTIFFField(new TIFFField(base.getTag(296), 2));
// BitsPerSample
rootIFD.addTIFFField(new TIFFField(base.getTag(258), 1));
// RowsPerStrip
rootIFD.addTIFFField(new TIFFField(base.getTag(278), image.getHeight()));
// FillOrder
rootIFD.addTIFFField(new TIFFField(base.getTag(266), 1));
return tiffMetadata;
}
private IIOMetadata getIIOMetadata(RenderedImage image,
ImageWriter imageWriter, ImageWriteParam param) {
ImageTypeSpecifier spec = ImageTypeSpecifier
.createFromRenderedImage(image);
IIOMetadata metadata = imageWriter.getDefaultImageMetadata(spec, param);
return metadata;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment