blob: 3afc75f73557dbe6c21c2607406f11adb4f3e508 [file] [log] [blame]
/**
* 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.pdfbox.jbig2.image;
import java.awt.Rectangle;
import java.awt.image.WritableRaster;
import org.apache.pdfbox.jbig2.Bitmap;
import org.apache.pdfbox.jbig2.util.Utils;
class Resizer {
static final class Mapping {
/** x and y scales */
final double scale;
/** x and y offset used by MAP, private fields */
final double offset = .5;
private final double a0;
private final double b0;
Mapping(double a0, double aw, double b0, double bw) {
this.a0 = a0;
this.b0 = b0;
scale = bw / aw;
if (scale <= 0.)
throw new IllegalArgumentException("Negative scales are not allowed");
}
Mapping(double scaleX) {
scale = scaleX;
a0 = b0 = 0;
}
double mapPixelCenter(final int b) {
return (b + offset - b0) / scale + a0;
}
double dstToSrc(final double b) {
return (b - b0) / scale + a0;
}
double srcToDst(final double a) {
return (a - a0) * scale + b0;
}
}
/**
* Order in which to apply filter
*/
private enum Order {
AUTO, XY, YX
}
/** Error tolerance */
private static final double EPSILON = 1e-7;
/** Number of bits in filter coefficients */
private int weightBits = 14;
private int weightOne = 1 << weightBits;
/** Number of bits per channel */
private int bitsPerChannel[] = new int[]{
8, 8, 8
};
private static final int NO_SHIFT[] = new int[16];
private int finalShift[] = new int[]{
2 * weightBits - bitsPerChannel[0], 2 * weightBits - bitsPerChannel[1], 2 * weightBits - bitsPerChannel[2]
};
/**
* Is x an integer?
*
* @param x the double to check
* @return <code>true</code> if x is an integer, <code>false</code> if not.
*/
private static boolean isInteger(final double x) {
return Math.abs(x - Math.floor(x + .5)) < EPSILON;
}
static final boolean debug = false;
/**
* Should filters be simplified if possible?
*/
private final boolean coerce = true;
/**
* The order in which data is processed.
*
* @see Order
*/
private final Order order = Order.AUTO;
/**
* Should zeros be trimmed in x filter weight tables?
*/
private final boolean trimZeros = true;
private final Mapping mappingX;
private final Mapping mappingY;
/**
* Creates an instance of {@link Resizer} with one scale factor for both x and y directions.
*
* @param scale the scale factor for x and y direction
*/
public Resizer(double scale) {
this(scale, scale);
}
/**
* Creates an instance of {@link Resizer} with a scale factor for each direction.
*
* @param scaleX the scale factor for x direction
* @param scaleY the scale factor for y direction
*/
public Resizer(double scaleX, double scaleY) {
mappingX = new Mapping(scaleX);
mappingY = new Mapping(scaleY);
}
private Weighttab[] createXWeights(Rectangle srcBounds, final Rectangle dstBounds, final ParameterizedFilter filter) {
final int srcX0 = srcBounds.x;
final int srcX1 = srcBounds.x + srcBounds.width;
final int dstX0 = dstBounds.x;
final int dstX1 = dstBounds.x + dstBounds.width;
final Weighttab tabs[] = new Weighttab[dstBounds.width];
for (int dstX = dstX0; dstX < dstX1; dstX++) {
final double center = mappingX.mapPixelCenter(dstX);
tabs[dstX - dstX0] = new Weighttab(filter, weightOne, center, srcX0, srcX1 - 1, trimZeros);
}
return tabs;
}
/**
* Checks if our discrete sampling of an arbitrary continuous filter, parameterized by the filter
* spacing ({@link ParameterizedFilter#scale}), its radius ({@link ParameterizedFilter#support}),
* and the scale and offset of the coordinate mapping, causes the filter to reduce to point
* sampling.
* <p>
* It reduces if support is less than 1 pixel or if integer scale and translation, and filter is
* cardinal.
*
* @param filter the parameterized filter instance to be simplified
* @param scale the scale of the coordinate mapping
* @param offset the offset of the coordinate mapping
*/
private ParameterizedFilter simplifyFilter(final ParameterizedFilter filter, final double scale, final double offset) {
if (coerce
&& (filter.support <= .5 || filter.filter.cardinal && isInteger(1. / filter.scale)
&& isInteger(1. / (scale * filter.scale)) && isInteger((offset / scale - .5) / filter.scale)))
return new ParameterizedFilter(new Filter.Point(), 1., .5, 1);
return filter;
}
/**
* Filtered zoom, x direction filtering before y direction filtering
* <p>
* Note: when calling {@link Resizer#createXWeights(Rectangle, Rectangle, ParameterizedFilter)},
* we can trim leading and trailing zeros from the x weight buffers as an optimization, but not
* for y weight buffers since the split formula is anticipating a constant amount of buffering of
* source scanlines; trimming zeros in y weight could cause feedback.
*/
private void resizeXfirst(final Object src, final Rectangle srcBounds, final Object dst, final Rectangle dstBounds,
final ParameterizedFilter xFilter, final ParameterizedFilter yFilter) {
// source scanline buffer
final Scanline buffer = createScanline(src, dst, srcBounds.width);
// accumulator buffer
final Scanline accumulator = createScanline(src, dst, dstBounds.width);
// a sampled filter for source pixels for each dest x position
final Weighttab xWeights[] = createXWeights(srcBounds, dstBounds, xFilter);
// Circular buffer of active lines
final int yBufferSize = yFilter.width + 2;
final Scanline lineBuffer[] = new Scanline[yBufferSize];
for (int y = 0; y < yBufferSize; y++) {
lineBuffer[y] = createScanline(src, dst, dstBounds.width);
lineBuffer[y].y = -1; /* mark scanline as unread */
}
// range of source and destination scanlines in regions
final int srcY0 = srcBounds.y;
final int srcY1 = srcBounds.y + srcBounds.height;
final int dstY0 = dstBounds.y;
final int dstY1 = dstBounds.y + dstBounds.height;
int yFetched = -1; // used to assert no backtracking
// loop over dest scanlines
for (int dstY = dstY0; dstY < dstY1; dstY++) {
// a sampled filter for source pixels for each dest x position
final Weighttab yWeight = new Weighttab(yFilter, weightOne, mappingY.mapPixelCenter(dstY), srcY0, srcY1 - 1, true);
accumulator.clear();
// loop over source scanlines that contribute to this dest scanline
for (int srcY = yWeight.i0; srcY <= yWeight.i1; srcY++) {
final Scanline srcBuffer = lineBuffer[srcY % yBufferSize];
if (debug)
System.out.println(" abuf.y / ayf " + srcBuffer.y + " / " + srcY);
if (srcBuffer.y != srcY) {
// scanline needs to be fetched from src raster
srcBuffer.y = srcY;
if (srcY0 + srcY <= yFetched)
throw new AssertionError("Backtracking from line " + yFetched + " to " + (srcY0 + srcY));
buffer.fetch(srcBounds.x, srcY0 + srcY);
yFetched = srcY0 + srcY;
// filter it into the appropriate line of linebuf (xfilt)
buffer.filter(NO_SHIFT, bitsPerChannel, xWeights, srcBuffer);
}
// add weighted tbuf into accum (these do yfilt)
srcBuffer.accumulate(yWeight.weights[srcY - yWeight.i0], accumulator);
}
accumulator.shift(finalShift);
accumulator.store(dstBounds.x, dstY);
if (debug)
System.out.printf("\n");
}
}
/**
* Filtered zoom, y direction filtering before x direction filtering
* */
private void resizeYfirst(final Object src, final Rectangle srcBounds, final Object dst, final Rectangle dstBounds,
final ParameterizedFilter xFilter, final ParameterizedFilter yFilter) {
// destination scanline buffer
final Scanline buffer = createScanline(src, dst, dstBounds.width);
// accumulator buffer
final Scanline accumulator = createScanline(src, dst, srcBounds.width);
// a sampled filter for source pixels for each destination x position
final Weighttab xWeights[] = createXWeights(srcBounds, dstBounds, xFilter);
// Circular buffer of active lines
final int yBufferSize = yFilter.width + 2;
final Scanline lineBuffer[] = new Scanline[yBufferSize];
for (int y = 0; y < yBufferSize; y++) {
lineBuffer[y] = createScanline(src, dst, srcBounds.width);
// mark scanline as unread
lineBuffer[y].y = -1;
}
// range of source and destination scanlines in regions
final int srcY0 = srcBounds.y;
final int srcY1 = srcBounds.y + srcBounds.height;
final int dstY0 = dstBounds.y;
final int dstY1 = dstBounds.y + dstBounds.height;
// used to assert no backtracking
int yFetched = -1;
// loop over destination scanlines
for (int dstY = dstY0; dstY < dstY1; dstY++) {
// prepare a weighttab for destination y position by a single sampled filter for current y
// position
final Weighttab yWeight = new Weighttab(yFilter, weightOne, mappingY.mapPixelCenter(dstY), srcY0, srcY1 - 1, true);
accumulator.clear();
// loop over source scanlines that contribute to this destination scanline
for (int srcY = yWeight.i0; srcY <= yWeight.i1; srcY++) {
final Scanline srcBuffer = lineBuffer[srcY % yBufferSize];
if (srcBuffer.y != srcY) {
// scanline needs to be fetched from source raster
srcBuffer.y = srcY;
if (srcY0 + srcY <= yFetched)
throw new AssertionError("Backtracking from line " + yFetched + " to " + (srcY0 + srcY));
srcBuffer.fetch(srcBounds.x, srcY0 + srcY);
yFetched = srcY0 + srcY;
}
if (debug)
System.out.println(dstY + "[] += " + srcY + "[] * " + yWeight.weights[srcY - yWeight.i0]);
// add weighted source buffer into accumulator (these do y filter)
srcBuffer.accumulate(yWeight.weights[srcY - yWeight.i0], accumulator);
}
// and filter it into the appropriate line of line buffer (x filter)
accumulator.filter(bitsPerChannel, finalShift, xWeights, buffer);
// store destination scanline into destination raster
buffer.store(dstBounds.x, dstY);
if (debug)
System.out.printf("\n");
}
}
/**
* @param src Source object
* @param srcBounds Bounds of the source object
* @param dst Destination object
* @param dstBounds Bounds of the destination object
* @param xFilter The filter used for x direction filtering
* @param yFilter The filter used for y direction filtering
*/
public void resize(final Object src, final Rectangle srcBounds, final Object dst, Rectangle dstBounds,
Filter xFilter, Filter yFilter) {
/*
* find scale of filter in a space (source space) when minifying, source scale=1/scale, but when
* magnifying, source scale=1
*/
ParameterizedFilter xFilterParameterized = new ParameterizedFilter(xFilter, mappingX.scale);
ParameterizedFilter yFilterParameterized = new ParameterizedFilter(yFilter, mappingY.scale);
/* find valid destination window (transformed source + support margin) */
final Rectangle dstRegion = new Rectangle();
final int x1 = Utils.ceil(mappingX.srcToDst(srcBounds.x - xFilterParameterized.support) + EPSILON);
final int y1 = Utils.ceil(mappingY.srcToDst(srcBounds.y - yFilterParameterized.support) + EPSILON);
final int x2 = Utils.floor(mappingX.srcToDst(srcBounds.x + srcBounds.width + xFilterParameterized.support)
- EPSILON);
final int y2 = Utils.floor(mappingY.srcToDst(srcBounds.y + srcBounds.height + yFilterParameterized.support)
- EPSILON);
dstRegion.setFrameFromDiagonal(x1, y1, x2, y2);
if (dstBounds.x < dstRegion.x || dstBounds.getMaxX() > dstRegion.getMaxX() || dstBounds.y < dstRegion.y
|| dstBounds.getMaxY() > dstRegion.getMaxY()) {
/* requested destination window lies outside the valid destination, so clip destination */
dstBounds = dstBounds.intersection(dstRegion);
}
if (srcBounds.isEmpty() || dstBounds.width <= 0 || dstBounds.height <= 0) {
return;
}
/* check for high-level simplifications of filter */
xFilterParameterized = simplifyFilter(xFilterParameterized, mappingX.scale, mappingX.offset);
yFilterParameterized = simplifyFilter(yFilterParameterized, mappingY.scale, mappingY.offset);
/*
* decide which filtering order (x->y or y->x) is faster for this mapping by counting
* convolution multiplies
*/
final boolean orderXY = order != Order.AUTO
? order == Order.XY
: dstBounds.width
* (srcBounds.height * xFilterParameterized.width + dstBounds.height * yFilterParameterized.width) < dstBounds.height
* (dstBounds.width * xFilterParameterized.width + srcBounds.width * yFilterParameterized.width);
// choose most efficient filtering order
if (orderXY) {
resizeXfirst(src, srcBounds, dst, dstBounds, xFilterParameterized, yFilterParameterized);
} else {
resizeYfirst(src, srcBounds, dst, dstBounds, xFilterParameterized, yFilterParameterized);
}
}
private static Scanline createScanline(final Object src, Object dst, final int length) {
if (src == null)
throw new IllegalArgumentException("src must not be null");
if (!(src instanceof Bitmap))
throw new IllegalArgumentException("src must be from type " + Bitmap.class.getName());
if (dst == null)
throw new IllegalArgumentException("dst must not be null");
if (!(dst instanceof WritableRaster))
throw new IllegalArgumentException("dst must be from type " + WritableRaster.class.getName());
return new BitmapScanline((Bitmap) src, (WritableRaster) dst, length);
}
}