Skip to content

Commit

Permalink
Optimize image operations (#422)
Browse files Browse the repository at this point in the history
  • Loading branch information
TomRoush committed Jun 28, 2022
1 parent c093bc0 commit ea01903
Show file tree
Hide file tree
Showing 2 changed files with 159 additions and 75 deletions.
Expand Up @@ -563,74 +563,137 @@ public Bitmap getOpaqueImage() throws IOException
return SampledImageReader.getRGBImage(this, null);
}

// explicit mask: RGB + Binary -> ARGB
// soft mask: RGB + Gray -> ARGB
private Bitmap applyMask(Bitmap image, Bitmap mask,
boolean isSoft, float[] matte)
/**
* @param image The image to apply the mask to as alpha channel.
* @param mask A mask image in 8 bit Gray. Even for a stencil mask image due to
* {@link #getOpaqueImage()} and {@link SampledImageReader}'s {@code from1Bit()} special
* handling of DeviceGray.
* @param isSoft {@code true} if a soft mask. If not stencil mask, then alpha will be inverted
* by this method.
* @param matte an optional RGB matte if a soft mask.
* @return an ARGB image (can be the altered original image)
*/
private Bitmap applyMask(Bitmap image, Bitmap mask, boolean isSoft, float[] matte)
{
if (mask == null)
{
return image;
}

int width = image.getWidth();
int height = image.getHeight();
final int width = Math.max(image.getWidth(), mask.getWidth());
final int height = Math.max(image.getHeight(), mask.getHeight());

// scale mask to fit image, or image to fit mask, whichever is larger
// scale mask to fit image, or image to fit mask, whichever is larger.
// also make sure that mask is 8 bit gray and image is ARGB as this
// is what needs to be returned.
if (mask.getWidth() < width || mask.getHeight() < height)
{
mask = scaleImage(mask, width, height);
}

if (mask.getWidth() > width || mask.getHeight() > height)
if (image.getWidth() < width || image.getHeight() < height)
{
width = mask.getWidth();
height = mask.getHeight();
image = scaleImage(image, width, height);
}
if (image.getConfig() != Bitmap.Config.ARGB_8888 || !image.isMutable())
{
image = image.copy(Bitmap.Config.ARGB_8888, true);
}
int[] pixels = new int[width];
int[] maskPixels = new int[width];

// compose to ARGB
Bitmap masked = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
int[] destRow = new int[width];

int r, g, b, alpha;
int[] alphaRow = new int[width];
int[] rgbaRow = new int[width];
for (int y = 0; y < height; y++)
// compose alpha into ARGB image, either:
// - very fast by direct bit combination if not a soft mask and a 8 bit alpha source.
// - fast by letting the sample model do a bulk band operation if no matte is set.
// - slow and complex by matte calculations on individual pixel components.
if (!isSoft && image.getByteCount() == mask.getByteCount())
{
image.getPixels(rgbaRow, 0, width, 0, y, width, 1);
mask.getPixels(alphaRow, 0, width, 0, y, width, 1);
for (int x = 0; x < width; x++)
for (int y = 0; y < height; y++)
{
r = Color.red(rgbaRow[x]);
g = Color.green(rgbaRow[x]);
b = Color.blue(rgbaRow[x]);
if (isSoft)
image.getPixels(pixels, 0, width, 0, y, width, 1);
mask.getPixels(maskPixels, 0, width, 0, y, width, 1);
for (int i = 0, c = width; c > 0; i++, c--)
{
alpha = Color.alpha(alphaRow[x]);
if (matte != null && alpha != 0)
pixels[i] = pixels[i] & 0xffffff | ~maskPixels[i] & 0xff000000;
}
image.setPixels(pixels, 0, width, 0, y, width, 1);
}
}
else if (matte == null)
{
for (int y = 0; y < height; y++)
{
image.getPixels(pixels, 0, width, 0, y, width, 1);
mask.getPixels(maskPixels, 0, width, 0, y, width, 1);
for (int x = 0; x < width; x++)
{
if (!isSoft)
{
float k = alpha / 255F;
r = clampColor(((r / 255f - matte[0]) / k + matte[0]) * 255);
g = clampColor(((g / 255f - matte[1]) / k + matte[1]) * 255);
b = clampColor(((b / 255f - matte[2]) / k + matte[2]) * 255);
maskPixels[x] ^= -1;
}
pixels[x] = pixels[x] & 0xffffff | maskPixels[x] & 0xff000000;
}
else
image.setPixels(pixels, 0, width, 0, y, width, 1);
}
}
else
{
// Original code is to clamp component and alpha to [0f, 1f] as matte is,
// and later expand to [0; 255] again (with rounding).
// component = 255f * ((component / 255f - matte) / (alpha / 255f) + matte)
// = (255 * component - 255 * 255f * matte) / alpha + 255f * matte
// There is a clearly visible factor 255 for most components in above formula,
// i.e. max value is 255 * 255: 16 bits + sign.
// Let's use faster fixed point integer arithmetics with Q16.15,
// introducing neglible errors (0.001%).
// Note: For "correct" rounding we increase the final matte value (m0h, m1h, m2h) by
// a half an integer.
final int fraction = 15;
final int factor = 255 << fraction;
final int m0 = Math.round(factor * matte[0]) * 255;
final int m1 = Math.round(factor * matte[1]) * 255;
final int m2 = Math.round(factor * matte[2]) * 255;
final int m0h = m0 / 255 + (1 << fraction - 1);
final int m1h = m1 / 255 + (1 << fraction - 1);
final int m2h = m2 / 255 + (1 << fraction - 1);
for (int y = 0; y < height; y++)
{
image.getPixels(pixels, 0, width, 0, y, width, 1);
mask.getPixels(maskPixels, 0, width, 0, y, width, 1);
for (int x = 0; x < width; x++)
{
alpha = 255 - Color.alpha(alphaRow[x]);
int a = Color.alpha(maskPixels[x]);
if (a == 0)
{
pixels[x] = pixels[x] & 0xffffff;
continue;
}
int rgb = pixels[x];
int r = Color.red(rgb);
int g = Color.green(rgb);
int b = Color.blue(rgb);
r = clampColor(((r * factor - m0) / a + m0h) >> fraction);
g = clampColor(((g * factor - m1) / a + m1h) >> fraction);
b = clampColor(((b * factor - m2) / a + m2h) >> fraction);
pixels[x] = Color.argb(a, r, g, b);
}

destRow[x] = Color.argb(alpha, r, g, b);
image.setPixels(pixels, 0, width, 0, y, width, 1);
}
masked.setPixels(destRow, 0, width, 0, y, width, 1);
}
return masked;
return image;
}

private int clampColor(float color)
{
return color < 0 ? 0 : (color > 255 ? 255 : Math.round(color));
// Float.valueOf is no need and it is too slow
if (color <= 0)
{
return 0;
}
else if (color >= 255)
{
return 255;
}
return (int)color;
}

/**
Expand Down
Expand Up @@ -359,8 +359,8 @@ private static Bitmap from8bit(PDImage pdImage, Rect clipped, final int subsampl
try
{
final int inputWidth;
final int startx;
final int starty;
int startx;
int starty;
final int scanWidth;
final int scanHeight;
if (options.isFilterSubsampled())
Expand All @@ -383,51 +383,72 @@ private static Bitmap from8bit(PDImage pdImage, Rect clipped, final int subsampl
scanHeight = clipped.height();
}
final int numComponents = pdImage.getColorSpace().getNumberOfComponents();
// get the raster's underlying byte buffer
int[] banks = new int[width * height];
// byte[][] banks = ((DataBufferByte) raster.getDataBuffer()).getBankData();
byte[] tempBytes = new byte[numComponents * inputWidth];
// compromise between memory and time usage:
// reading the whole image consumes too much memory
// reading one pixel at a time makes it slow in our buffering infrastructure
int i = 0;
for (int y = 0; y < starty + scanHeight; ++y)
if (startx == 0 && starty == 0 && scanWidth == width && scanHeight == height)
{
IOUtils.populateBuffer(input, tempBytes);
if (y < starty || y % currentSubsampling > 0)
{
continue;
}

for (int x = startx; x < startx + scanWidth; x += currentSubsampling)
// we just need to copy all sample data, then convert to RGB image.
return createBitmapFromRawStream(input, inputWidth, numComponents, currentSubsampling);
}
else
{
Bitmap origin = createBitmapFromRawStream(input, inputWidth, numComponents,
currentSubsampling);
if (currentSubsampling > 1)
{
int tempBytesIdx = x * numComponents;
if (numComponents == 3)
{
banks[i] = Color.argb(255, tempBytes[tempBytesIdx] & 0xFF,
tempBytes[tempBytesIdx + 1] & 0xFF, tempBytes[tempBytesIdx + 2] & 0xFF);
}
else if (numComponents == 1)
{
int in = tempBytes[tempBytesIdx] & 0xFF;
banks[i] = Color.argb(in, in, in, in);
}
++i;
startx /= currentSubsampling;
starty /= currentSubsampling;
}
return Bitmap.createBitmap(origin, startx, starty, width, height);
}
Bitmap raster = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
raster.setPixels(banks, 0, width, 0 ,0, width, height);

// use the color space to convert the image to RGB
// return pdImage.getColorSpace().toRGBImage(raster); TODO: PdfBox-Android
return raster;
}
finally
{
IOUtils.closeQuietly(input);
}
}

private static Bitmap createBitmapFromRawStream(InputStream input, int originalWidth, int numComponents,
int sampleSize) throws IOException
{
byte[] bytes = IOUtils.toByteArray(input);
int originalHeight = bytes.length / numComponents / originalWidth;
if (numComponents == 1)
{
byte[] result = new byte[originalWidth * originalHeight * 4];
for (int i = originalWidth * originalHeight - 1; i >= 0; i--)
{
int to = i * 4;
result[to + 3] = bytes[i];
result[to] = bytes[i];
result[to + 1] = bytes[i];
result[to + 2] = bytes[i];
}
bytes = result;
}
else if (numComponents == 3)
{
byte[] result = new byte[originalWidth * originalHeight * 4];
for (int i = originalWidth * originalHeight - 1; i >= 0; i--)
{
int to = i * 4;
int from = i * 3;
result[to + 3] = (byte)255;
result[to] = bytes[from];
result[to + 1] = bytes[from + 1];
result[to + 2] = bytes[from + 2];
}
bytes = result;
}
Bitmap bitmap = Bitmap.createBitmap(originalWidth, originalHeight, Bitmap.Config.ARGB_8888);
bitmap.copyPixelsFromBuffer(ByteBuffer.wrap(bytes));
if (sampleSize > 1)
{
int width = originalWidth / sampleSize;
int height = originalHeight / sampleSize;
bitmap = Bitmap.createScaledBitmap(bitmap, width, height, true);
}
return bitmap;
}

// slower, general-purpose image conversion from any image format
// private static BufferedImage fromAny(PDImage pdImage, WritableRaster raster, COSArray colorKey, Rectangle clipped,
// final int subsampling, final int width, final int height) TODO: Pdfbox-Android
Expand Down

0 comments on commit ea01903

Please sign in to comment.