Skip to content

Commit ea01903

Browse files
authored
Optimize image operations (#422)
1 parent c093bc0 commit ea01903

File tree

2 files changed

+159
-75
lines changed

2 files changed

+159
-75
lines changed

library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/image/PDImageXObject.java

Lines changed: 102 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -563,74 +563,137 @@ public Bitmap getOpaqueImage() throws IOException
563563
return SampledImageReader.getRGBImage(this, null);
564564
}
565565

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

576-
int width = image.getWidth();
577-
int height = image.getHeight();
583+
final int width = Math.max(image.getWidth(), mask.getWidth());
584+
final int height = Math.max(image.getHeight(), mask.getHeight());
578585

579-
// scale mask to fit image, or image to fit mask, whichever is larger
586+
// scale mask to fit image, or image to fit mask, whichever is larger.
587+
// also make sure that mask is 8 bit gray and image is ARGB as this
588+
// is what needs to be returned.
580589
if (mask.getWidth() < width || mask.getHeight() < height)
581590
{
582591
mask = scaleImage(mask, width, height);
583592
}
584-
585-
if (mask.getWidth() > width || mask.getHeight() > height)
593+
if (image.getWidth() < width || image.getHeight() < height)
586594
{
587-
width = mask.getWidth();
588-
height = mask.getHeight();
589595
image = scaleImage(image, width, height);
590596
}
597+
if (image.getConfig() != Bitmap.Config.ARGB_8888 || !image.isMutable())
598+
{
599+
image = image.copy(Bitmap.Config.ARGB_8888, true);
600+
}
601+
int[] pixels = new int[width];
602+
int[] maskPixels = new int[width];
591603

592-
// compose to ARGB
593-
Bitmap masked = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
594-
int[] destRow = new int[width];
595-
596-
int r, g, b, alpha;
597-
int[] alphaRow = new int[width];
598-
int[] rgbaRow = new int[width];
599-
for (int y = 0; y < height; y++)
604+
// compose alpha into ARGB image, either:
605+
// - very fast by direct bit combination if not a soft mask and a 8 bit alpha source.
606+
// - fast by letting the sample model do a bulk band operation if no matte is set.
607+
// - slow and complex by matte calculations on individual pixel components.
608+
if (!isSoft && image.getByteCount() == mask.getByteCount())
600609
{
601-
image.getPixels(rgbaRow, 0, width, 0, y, width, 1);
602-
mask.getPixels(alphaRow, 0, width, 0, y, width, 1);
603-
for (int x = 0; x < width; x++)
610+
for (int y = 0; y < height; y++)
604611
{
605-
r = Color.red(rgbaRow[x]);
606-
g = Color.green(rgbaRow[x]);
607-
b = Color.blue(rgbaRow[x]);
608-
if (isSoft)
612+
image.getPixels(pixels, 0, width, 0, y, width, 1);
613+
mask.getPixels(maskPixels, 0, width, 0, y, width, 1);
614+
for (int i = 0, c = width; c > 0; i++, c--)
609615
{
610-
alpha = Color.alpha(alphaRow[x]);
611-
if (matte != null && alpha != 0)
616+
pixels[i] = pixels[i] & 0xffffff | ~maskPixels[i] & 0xff000000;
617+
}
618+
image.setPixels(pixels, 0, width, 0, y, width, 1);
619+
}
620+
}
621+
else if (matte == null)
622+
{
623+
for (int y = 0; y < height; y++)
624+
{
625+
image.getPixels(pixels, 0, width, 0, y, width, 1);
626+
mask.getPixels(maskPixels, 0, width, 0, y, width, 1);
627+
for (int x = 0; x < width; x++)
628+
{
629+
if (!isSoft)
612630
{
613-
float k = alpha / 255F;
614-
r = clampColor(((r / 255f - matte[0]) / k + matte[0]) * 255);
615-
g = clampColor(((g / 255f - matte[1]) / k + matte[1]) * 255);
616-
b = clampColor(((b / 255f - matte[2]) / k + matte[2]) * 255);
631+
maskPixels[x] ^= -1;
617632
}
633+
pixels[x] = pixels[x] & 0xffffff | maskPixels[x] & 0xff000000;
618634
}
619-
else
635+
image.setPixels(pixels, 0, width, 0, y, width, 1);
636+
}
637+
}
638+
else
639+
{
640+
// Original code is to clamp component and alpha to [0f, 1f] as matte is,
641+
// and later expand to [0; 255] again (with rounding).
642+
// component = 255f * ((component / 255f - matte) / (alpha / 255f) + matte)
643+
// = (255 * component - 255 * 255f * matte) / alpha + 255f * matte
644+
// There is a clearly visible factor 255 for most components in above formula,
645+
// i.e. max value is 255 * 255: 16 bits + sign.
646+
// Let's use faster fixed point integer arithmetics with Q16.15,
647+
// introducing neglible errors (0.001%).
648+
// Note: For "correct" rounding we increase the final matte value (m0h, m1h, m2h) by
649+
// a half an integer.
650+
final int fraction = 15;
651+
final int factor = 255 << fraction;
652+
final int m0 = Math.round(factor * matte[0]) * 255;
653+
final int m1 = Math.round(factor * matte[1]) * 255;
654+
final int m2 = Math.round(factor * matte[2]) * 255;
655+
final int m0h = m0 / 255 + (1 << fraction - 1);
656+
final int m1h = m1 / 255 + (1 << fraction - 1);
657+
final int m2h = m2 / 255 + (1 << fraction - 1);
658+
for (int y = 0; y < height; y++)
659+
{
660+
image.getPixels(pixels, 0, width, 0, y, width, 1);
661+
mask.getPixels(maskPixels, 0, width, 0, y, width, 1);
662+
for (int x = 0; x < width; x++)
620663
{
621-
alpha = 255 - Color.alpha(alphaRow[x]);
664+
int a = Color.alpha(maskPixels[x]);
665+
if (a == 0)
666+
{
667+
pixels[x] = pixels[x] & 0xffffff;
668+
continue;
669+
}
670+
int rgb = pixels[x];
671+
int r = Color.red(rgb);
672+
int g = Color.green(rgb);
673+
int b = Color.blue(rgb);
674+
r = clampColor(((r * factor - m0) / a + m0h) >> fraction);
675+
g = clampColor(((g * factor - m1) / a + m1h) >> fraction);
676+
b = clampColor(((b * factor - m2) / a + m2h) >> fraction);
677+
pixels[x] = Color.argb(a, r, g, b);
622678
}
623-
624-
destRow[x] = Color.argb(alpha, r, g, b);
679+
image.setPixels(pixels, 0, width, 0, y, width, 1);
625680
}
626-
masked.setPixels(destRow, 0, width, 0, y, width, 1);
627681
}
628-
return masked;
682+
return image;
629683
}
630684

631685
private int clampColor(float color)
632686
{
633-
return color < 0 ? 0 : (color > 255 ? 255 : Math.round(color));
687+
// Float.valueOf is no need and it is too slow
688+
if (color <= 0)
689+
{
690+
return 0;
691+
}
692+
else if (color >= 255)
693+
{
694+
return 255;
695+
}
696+
return (int)color;
634697
}
635698

636699
/**

library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/image/SampledImageReader.java

Lines changed: 57 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -359,8 +359,8 @@ private static Bitmap from8bit(PDImage pdImage, Rect clipped, final int subsampl
359359
try
360360
{
361361
final int inputWidth;
362-
final int startx;
363-
final int starty;
362+
int startx;
363+
int starty;
364364
final int scanWidth;
365365
final int scanHeight;
366366
if (options.isFilterSubsampled())
@@ -383,51 +383,72 @@ private static Bitmap from8bit(PDImage pdImage, Rect clipped, final int subsampl
383383
scanHeight = clipped.height();
384384
}
385385
final int numComponents = pdImage.getColorSpace().getNumberOfComponents();
386-
// get the raster's underlying byte buffer
387-
int[] banks = new int[width * height];
388-
// byte[][] banks = ((DataBufferByte) raster.getDataBuffer()).getBankData();
389-
byte[] tempBytes = new byte[numComponents * inputWidth];
390-
// compromise between memory and time usage:
391-
// reading the whole image consumes too much memory
392-
// reading one pixel at a time makes it slow in our buffering infrastructure
393-
int i = 0;
394-
for (int y = 0; y < starty + scanHeight; ++y)
386+
if (startx == 0 && starty == 0 && scanWidth == width && scanHeight == height)
395387
{
396-
IOUtils.populateBuffer(input, tempBytes);
397-
if (y < starty || y % currentSubsampling > 0)
398-
{
399-
continue;
400-
}
401-
402-
for (int x = startx; x < startx + scanWidth; x += currentSubsampling)
388+
// we just need to copy all sample data, then convert to RGB image.
389+
return createBitmapFromRawStream(input, inputWidth, numComponents, currentSubsampling);
390+
}
391+
else
392+
{
393+
Bitmap origin = createBitmapFromRawStream(input, inputWidth, numComponents,
394+
currentSubsampling);
395+
if (currentSubsampling > 1)
403396
{
404-
int tempBytesIdx = x * numComponents;
405-
if (numComponents == 3)
406-
{
407-
banks[i] = Color.argb(255, tempBytes[tempBytesIdx] & 0xFF,
408-
tempBytes[tempBytesIdx + 1] & 0xFF, tempBytes[tempBytesIdx + 2] & 0xFF);
409-
}
410-
else if (numComponents == 1)
411-
{
412-
int in = tempBytes[tempBytesIdx] & 0xFF;
413-
banks[i] = Color.argb(in, in, in, in);
414-
}
415-
++i;
397+
startx /= currentSubsampling;
398+
starty /= currentSubsampling;
416399
}
400+
return Bitmap.createBitmap(origin, startx, starty, width, height);
417401
}
418-
Bitmap raster = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
419-
raster.setPixels(banks, 0, width, 0 ,0, width, height);
420-
421-
// use the color space to convert the image to RGB
422-
// return pdImage.getColorSpace().toRGBImage(raster); TODO: PdfBox-Android
423-
return raster;
424402
}
425403
finally
426404
{
427405
IOUtils.closeQuietly(input);
428406
}
429407
}
430408

409+
private static Bitmap createBitmapFromRawStream(InputStream input, int originalWidth, int numComponents,
410+
int sampleSize) throws IOException
411+
{
412+
byte[] bytes = IOUtils.toByteArray(input);
413+
int originalHeight = bytes.length / numComponents / originalWidth;
414+
if (numComponents == 1)
415+
{
416+
byte[] result = new byte[originalWidth * originalHeight * 4];
417+
for (int i = originalWidth * originalHeight - 1; i >= 0; i--)
418+
{
419+
int to = i * 4;
420+
result[to + 3] = bytes[i];
421+
result[to] = bytes[i];
422+
result[to + 1] = bytes[i];
423+
result[to + 2] = bytes[i];
424+
}
425+
bytes = result;
426+
}
427+
else if (numComponents == 3)
428+
{
429+
byte[] result = new byte[originalWidth * originalHeight * 4];
430+
for (int i = originalWidth * originalHeight - 1; i >= 0; i--)
431+
{
432+
int to = i * 4;
433+
int from = i * 3;
434+
result[to + 3] = (byte)255;
435+
result[to] = bytes[from];
436+
result[to + 1] = bytes[from + 1];
437+
result[to + 2] = bytes[from + 2];
438+
}
439+
bytes = result;
440+
}
441+
Bitmap bitmap = Bitmap.createBitmap(originalWidth, originalHeight, Bitmap.Config.ARGB_8888);
442+
bitmap.copyPixelsFromBuffer(ByteBuffer.wrap(bytes));
443+
if (sampleSize > 1)
444+
{
445+
int width = originalWidth / sampleSize;
446+
int height = originalHeight / sampleSize;
447+
bitmap = Bitmap.createScaledBitmap(bitmap, width, height, true);
448+
}
449+
return bitmap;
450+
}
451+
431452
// slower, general-purpose image conversion from any image format
432453
// private static BufferedImage fromAny(PDImage pdImage, WritableRaster raster, COSArray colorKey, Rectangle clipped,
433454
// final int subsampling, final int width, final int height) TODO: Pdfbox-Android

0 commit comments

Comments
 (0)