BlankImageCheck.java
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.tika.imageprocessing;
import java.io.IOException;
import org.apache.tika.annotation.TikaComponent;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.parser.ParseContext;
import org.apache.tika.parser.enricher.ImageCheck;
import org.apache.tika.parser.enricher.ImageProbe;
/**
* Refuses a blank image: the share of pixels whose gray level differs from the image's most
* common gray level (the background) by more than {@code tolerance} is at most {@code maxInk}.
* Measured on a copy of at most 1,000 pixels a side. The default {@code maxInk}, 0.0001
* (0.01 %), is below a page with a few lines of text: on 13,641 PDF pages it flagged the 87 with
* no ink at all and 3 with a single line. With {@code despeckle} (the default) a pixel counts as
* ink only if most of its 3 x 3 neighbourhood does, so scattered specks (a noisy scan) do not
* hide a blank page: salt-and-pepper noise of 0.1 % to 5 % comes out blank, and none of 600 text
* pages did. An image with transparency is blank only if it is blank over white and over black.
* An image it cannot read or decode is admitted.
*
* @since Apache Tika 4.2
*/
@TikaComponent(name = "blank-image-check")
public class BlankImageCheck implements ImageCheck {
private static final long serialVersionUID = 1L;
private double maxInk = 0.0001;
private int tolerance = 40;
private boolean despeckle = true;
@Override
public Verdict check(ImageProbe image, Metadata metadata, ParseContext context)
throws IOException {
ImageDecoding.Gray gray = ImageDecoding.gray(image);
if (gray == null || gray.levels().length == 0) {
return Verdict.admit();
}
boolean blank = ink(gray.levels(), gray.width(), gray.height()) <= maxInk
&& (gray.overBlack() == null
|| ink(gray.overBlack(), gray.width(), gray.height()) <= maxInk);
return blank ? Verdict.skip("blank") : Verdict.admit();
}
/** The share of pixels that differ from the background, after the specks are removed. */
private double ink(byte[] levels, int width, int height) {
int[] histogram = new int[256];
for (byte level : levels) {
histogram[level & 255]++;
}
int mode = 0;
for (int i = 1; i < 256; i++) {
if (histogram[i] > histogram[mode]) {
mode = i;
}
}
byte[] mask = new byte[levels.length];
for (int i = 0; i < levels.length; i++) {
mask[i] = (byte) (Math.abs((levels[i] & 255) - mode) > tolerance ? 1 : 0);
}
long ink = 0;
if (!despeckle) {
for (byte m : mask) {
ink += m;
}
} else {
// a 3 x 3 median of a binary mask is a majority vote: isolated specks, as on a noisy
// scan, are outvoted by the background, a stroke of text is not
int[] horizontal = new int[levels.length];
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int i = y * width + x;
horizontal[i] = mask[i] + (x > 0 ? mask[i - 1] : 0)
+ (x < width - 1 ? mask[i + 1] : 0);
}
}
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int i = y * width + x;
int votes = horizontal[i] + (y > 0 ? horizontal[i - width] : 0)
+ (y < height - 1 ? horizontal[i + width] : 0);
if (votes >= 5) {
ink++;
}
}
}
}
return (double) ink / levels.length;
}
public boolean isDespeckle() {
return despeckle;
}
public void setDespeckle(boolean despeckle) {
this.despeckle = despeckle;
}
public double getMaxInk() {
return maxInk;
}
public void setMaxInk(double maxInk) {
if (!(maxInk >= 0 && maxInk < 1)) {
throw new IllegalArgumentException("maxInk must be at least 0 and below 1, got: "
+ maxInk);
}
this.maxInk = maxInk;
}
public int getTolerance() {
return tolerance;
}
public void setTolerance(int tolerance) {
if (tolerance < 0 || tolerance > 255) {
throw new IllegalArgumentException("tolerance must be 0 to 255, got: " + tolerance);
}
this.tolerance = tolerance;
}
}