From fcb7229addde6203fcbe70f353ccbe0d9960f446 Mon Sep 17 00:00:00 2001 From: Sandeep Singh Date: Sat, 3 Oct 2026 01:58:25 +0530 Subject: [PATCH 1/6] Implement PDIndexed color space and fix soft mask luminance in applyMask --- .../pdmodel/graphics/color/PDColorSpace.java | 4 +- .../pdmodel/graphics/color/PDIndexed.java | 268 ++++++++++++++++++ .../graphics/image/PDImageXObject.java | 37 +-- .../graphics/image/SampledImageReader.java | 33 ++- 4 files changed, 306 insertions(+), 36 deletions(-) create mode 100644 library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDIndexed.java diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDColorSpace.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDColorSpace.java index 52ecd72f..090cec5b 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDColorSpace.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDColorSpace.java @@ -186,9 +186,7 @@ else if (name == COSName.DEVICEN) } else if (name == COSName.INDEXED) { -// return new PDIndexed(array); - Log.e("PdfBox-Android", "Unsupported color space kind: " + name + ". Will try DeviceRGB instead"); - return PDDeviceRGB.INSTANCE; + return new PDIndexed(array, resources); } else if (name == COSName.SEPARATION) { diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDIndexed.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDIndexed.java new file mode 100644 index 00000000..ecf5ab92 --- /dev/null +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDIndexed.java @@ -0,0 +1,268 @@ +/* + * 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 com.tom_roush.pdfbox.pdmodel.graphics.color; + +import android.graphics.Bitmap; +import android.graphics.Color; + +import java.io.IOException; + +import com.tom_roush.pdfbox.cos.COSArray; +import com.tom_roush.pdfbox.cos.COSBase; +import com.tom_roush.pdfbox.cos.COSInteger; +import com.tom_roush.pdfbox.cos.COSName; +import com.tom_roush.pdfbox.cos.COSNull; +import com.tom_roush.pdfbox.cos.COSNumber; +import com.tom_roush.pdfbox.cos.COSStream; +import com.tom_roush.pdfbox.cos.COSString; +import com.tom_roush.pdfbox.pdmodel.PDResources; +import com.tom_roush.pdfbox.pdmodel.common.PDStream; + +/** + * An Indexed colour space specifies that an area is to be painted using a colour table + * of arbitrary colours from another color space. + * + * @author John Hewson + * @author Ben Litchfield + */ +public final class PDIndexed extends PDSpecialColorSpace +{ + private final PDColor initialColor = new PDColor(new float[] { 0 }, this); + + private PDColorSpace baseColorSpace = null; + + // cached lookup data + private byte[] lookupData; + private float[][] colorTable; + private int actualMaxIndex; + private int[] rgbColorTable; + + public PDIndexed() + { + array = new COSArray(); + array.add(COSName.INDEXED); + array.add(COSName.DEVICERGB); + array.add(COSInteger.get(255)); + array.add(COSNull.NULL); + } + + public PDIndexed(COSArray indexedArray) throws IOException + { + this(indexedArray, null); + } + + public PDIndexed(COSArray indexedArray, PDResources resources) throws IOException + { + array = indexedArray; + baseColorSpace = PDColorSpace.create(array.get(1), resources); + readColorTable(); + initRgbColorTable(); + } + + @Override + public String getName() + { + return COSName.INDEXED.getName(); + } + + @Override + public int getNumberOfComponents() + { + return 1; + } + + @Override + public float[] getDefaultDecode(int bitsPerComponent) + { + return new float[] { 0, (float) Math.pow(2, bitsPerComponent) - 1 }; + } + + @Override + public PDColor getInitialColor() + { + return initialColor; + } + + private void initRgbColorTable() throws IOException + { + if (baseColorSpace == null) + { + rgbColorTable = new int[actualMaxIndex + 1]; + return; + } + + int numBaseComponents = baseColorSpace.getNumberOfComponents(); + rgbColorTable = new int[actualMaxIndex + 1]; + + float[] base = new float[numBaseComponents]; + for (int i = 0; i <= actualMaxIndex; i++) + { + for (int c = 0; c < numBaseComponents; c++) + { + base[c] = colorTable[i][c]; + } + float[] rgb = baseColorSpace.toRGB(base); + int r = Math.round(rgb[0] * 255f); + int g = Math.round(rgb[1] * 255f); + int b = Math.round(rgb[2] * 255f); + r = Math.max(0, Math.min(255, r)); + g = Math.max(0, Math.min(255, g)); + b = Math.max(0, Math.min(255, b)); + rgbColorTable[i] = Color.argb(255, r, g, b); + } + } + + @Override + public float[] toRGB(float[] value) + { + if (value.length != 1) + { + throw new IllegalArgumentException("Indexed color spaces must have one color value"); + } + + int index = Math.round(value[0]); + index = Math.max(0, Math.min(index, actualMaxIndex)); + + int argb = rgbColorTable[index]; + return new float[] { + Color.red(argb) / 255f, + Color.green(argb) / 255f, + Color.blue(argb) / 255f + }; + } + + /** + * Returns the ARGB packed integer color for a given palette index. + * + * @param index the palette index + * @return 0xAARRGGBB packed color + */ + public int getRgbColor(int index) + { + if (rgbColorTable == null || rgbColorTable.length == 0) + { + return Color.BLACK; + } + index = Math.max(0, Math.min(index, actualMaxIndex)); + return rgbColorTable[index]; + } + + public int[] getRgbColorTable() + { + return rgbColorTable; + } + + @Override + public Bitmap toRGBImage(Bitmap raster) throws IOException + { + int width = raster.getWidth(); + int height = raster.getHeight(); + int[] src = new int[width]; + int[] out = new int[width]; + + Bitmap rgbImage = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + for (int y = 0; y < height; y++) + { + raster.getPixels(src, 0, width, 0, y, width, 1); + for (int x = 0; x < width; x++) + { + int index = (src[x] >> 24) & 0xFF; + if (raster.getConfig() != Bitmap.Config.ALPHA_8) + { + index = src[x] & 0xFF; + } + out[x] = getRgbColor(index); + } + rgbImage.setPixels(out, 0, width, 0, y, width, 1); + } + return rgbImage; + } + + public PDColorSpace getBaseColorSpace() + { + return baseColorSpace; + } + + private int getHival() + { + COSBase base = array.getObject(2); + if (base instanceof COSNumber) + { + return ((COSNumber) base).intValue(); + } + return 255; + } + + private void readLookupData() throws IOException + { + if (lookupData == null) + { + COSBase lookupTable = array.getObject(3); + if (lookupTable instanceof COSString) + { + lookupData = ((COSString) lookupTable).getBytes(); + } + else if (lookupTable instanceof COSStream) + { + lookupData = new PDStream((COSStream) lookupTable).toByteArray(); + } + else if (lookupTable == null) + { + lookupData = new byte[0]; + } + else + { + throw new IOException("Error: Unknown type for lookup table " + lookupTable); + } + } + } + + private void readColorTable() throws IOException + { + readLookupData(); + + int maxIndex = Math.min(getHival(), 255); + int numComponents = baseColorSpace != null ? baseColorSpace.getNumberOfComponents() : 3; + + if (numComponents > 0 && lookupData.length / numComponents < maxIndex + 1) + { + maxIndex = lookupData.length / numComponents - 1; + } + actualMaxIndex = Math.max(0, maxIndex); + + colorTable = new float[actualMaxIndex + 1][numComponents]; + for (int i = 0, offset = 0; i <= actualMaxIndex; i++) + { + for (int c = 0; c < numComponents; c++) + { + if (offset < lookupData.length) + { + colorTable[i][c] = (lookupData[offset] & 0xff) / 255f; + offset++; + } + } + } + } + + @Override + public String toString() + { + return "Indexed{base:" + baseColorSpace + " " + + "hival:" + getHival() + " " + + "lookup:(" + (colorTable != null ? colorTable.length : 0) + " entries)}"; + } +} diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/image/PDImageXObject.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/image/PDImageXObject.java index 68718b3c..b6433f61 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/image/PDImageXObject.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/image/PDImageXObject.java @@ -640,16 +640,10 @@ private Bitmap applyMask(Bitmap image, Bitmap mask, boolean interpolateMask, final int height = Math.max(image.getHeight(), mask.getHeight()); // 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, interpolateMask); } - if (mask.getConfig() != Bitmap.Config.ALPHA_8 || !image.isMutable()) - { - mask = mask.copy(Bitmap.Config.ALPHA_8, true); - } if (image.getWidth() < width || image.getHeight() < height) { @@ -659,27 +653,11 @@ private Bitmap applyMask(Bitmap image, Bitmap mask, boolean interpolateMask, { image = image.copy(Bitmap.Config.ARGB_8888, true); } + final boolean isMaskAlpha8 = mask.getConfig() == Bitmap.Config.ALPHA_8; int[] pixels = new int[width]; int[] maskPixels = new int[width]; - // 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()) - { - 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 i = 0, c = width; c > 0; i++, c--) - { - pixels[i] = pixels[i] & 0xffffff | ~maskPixels[i] & 0xff000000; - } - image.setPixels(pixels, 0, width, 0, y, width, 1); - } - } - else if (matte == null) + if (matte == null) { for (int y = 0; y < height; y++) { @@ -687,11 +665,13 @@ else if (matte == null) mask.getPixels(maskPixels, 0, width, 0, y, width, 1); for (int x = 0; x < width; x++) { + int maskVal = maskPixels[x]; + int a = isMaskAlpha8 ? ((maskVal >> 24) & 0xFF) : ((maskVal >> 16) & 0xFF); if (!isSoft) { - maskPixels[x] ^= -1; + a = 255 - a; } - pixels[x] = pixels[x] & 0xffffff | maskPixels[x] & 0xff000000; + pixels[x] = (pixels[x] & 0x00FFFFFF) | (a << 24); } image.setPixels(pixels, 0, width, 0, y, width, 1); } @@ -722,10 +702,11 @@ else if (matte == null) mask.getPixels(maskPixels, 0, width, 0, y, width, 1); for (int x = 0; x < width; x++) { - int a = Color.alpha(maskPixels[x]); + int maskVal = maskPixels[x]; + int a = isMaskAlpha8 ? ((maskVal >> 24) & 0xFF) : ((maskVal >> 16) & 0xFF); if (a == 0) { - pixels[x] = pixels[x] & 0xffffff; + pixels[x] = pixels[x] & 0x00FFFFFF; continue; } int rgb = pixels[x]; diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/image/SampledImageReader.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/image/SampledImageReader.java index c3bd5fba..2aa1a14f 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/image/SampledImageReader.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/image/SampledImageReader.java @@ -36,6 +36,7 @@ import com.tom_roush.pdfbox.filter.DecodeOptions; import com.tom_roush.pdfbox.io.IOUtils; import com.tom_roush.pdfbox.pdmodel.graphics.color.PDColorSpace; +import com.tom_roush.pdfbox.pdmodel.graphics.color.PDIndexed; /** * Reads a sampled image from a PDF file. @@ -284,7 +285,7 @@ private static Bitmap from1Bit(PDImage pdImage, Rect clipped, final int subsampl // and then simply shift bits out to the left, detecting set bits via sign final boolean nosubsampling = currentSubsampling == 1; final int stride = (inputWidth + 7) / 8; - final int invert = /*colorSpace instanceof PDIndexed TODO: PdfBox-Android ||*/ decode[0] < decode[1] ? 0 : -1; + final int invert = (colorSpace instanceof PDIndexed || decode[0] < decode[1]) ? 0 : -1; final int endX = startx + scanWidth; final byte[] buff = new byte[stride]; for (int y = 0; y < starty + scanHeight; y++) @@ -367,15 +368,16 @@ private static Bitmap from8bit(PDImage pdImage, Rect clipped, final int subsampl scanHeight = clipped.height(); } final int numComponents = pdImage.getColorSpace().getNumberOfComponents(); + final PDColorSpace colorSpace = pdImage.getColorSpace(); if (startx == 0 && starty == 0 && scanWidth == width && scanHeight == height) { // we just need to copy all sample data, then convert to RGB image. - return createBitmapFromRawStream(input, inputWidth, numComponents, currentSubsampling); + return createBitmapFromRawStream(input, inputWidth, numComponents, currentSubsampling, colorSpace); } else { Bitmap origin = createBitmapFromRawStream(input, inputWidth, numComponents, - currentSubsampling); + currentSubsampling, colorSpace); if (currentSubsampling > 1) { startx /= currentSubsampling; @@ -391,17 +393,38 @@ private static Bitmap from8bit(PDImage pdImage, Rect clipped, final int subsampl } private static Bitmap createBitmapFromRawStream(InputStream input, int originalWidth, int numComponents, - int sampleSize) throws IOException + int sampleSize, PDColorSpace colorSpace) throws IOException { byte[] bytes = IOUtils.toByteArray(input); int originalHeight = bytes.length / numComponents / originalWidth; + + if (colorSpace instanceof PDIndexed) + { + PDIndexed indexed = (PDIndexed) colorSpace; + int[] pixels = new int[originalWidth * originalHeight]; + int len = Math.min(bytes.length, pixels.length); + for (int i = 0; i < len; i++) + { + pixels[i] = indexed.getRgbColor(bytes[i] & 0xFF); + } + Bitmap bitmap = Bitmap.createBitmap(originalWidth, originalHeight, Bitmap.Config.ARGB_8888); + bitmap.setPixels(pixels, 0, originalWidth, 0, 0, originalWidth, originalHeight); + if (sampleSize > 1) + { + int width = originalWidth / sampleSize; + int height = originalHeight / sampleSize; + bitmap = Bitmap.createScaledBitmap(bitmap, width, height, true); + } + return bitmap; + } + 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 + 3] = (byte) 255; result[to] = bytes[i]; result[to + 1] = bytes[i]; result[to + 2] = bytes[i]; From 873b7a3446c2a0b5130e66b36fac8074b61b9e4c Mon Sep 17 00:00:00 2001 From: Sandeep Singh Date: Sat, 3 Oct 2026 02:33:59 +0530 Subject: [PATCH 2/6] Add unit tests for PDIndexed and optimize color arithmetic with bitwise operations --- .../pdmodel/graphics/color/PDIndexed.java | 11 +- .../pdmodel/graphics/color/PDIndexedTest.java | 142 ++++++++++++++++++ 2 files changed, 147 insertions(+), 6 deletions(-) create mode 100644 library/src/test/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDIndexedTest.java diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDIndexed.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDIndexed.java index ecf5ab92..172fdde7 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDIndexed.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDIndexed.java @@ -17,7 +17,6 @@ package com.tom_roush.pdfbox.pdmodel.graphics.color; import android.graphics.Bitmap; -import android.graphics.Color; import java.io.IOException; @@ -122,7 +121,7 @@ private void initRgbColorTable() throws IOException r = Math.max(0, Math.min(255, r)); g = Math.max(0, Math.min(255, g)); b = Math.max(0, Math.min(255, b)); - rgbColorTable[i] = Color.argb(255, r, g, b); + rgbColorTable[i] = (0xFF << 24) | (r << 16) | (g << 8) | b; } } @@ -139,9 +138,9 @@ public float[] toRGB(float[] value) int argb = rgbColorTable[index]; return new float[] { - Color.red(argb) / 255f, - Color.green(argb) / 255f, - Color.blue(argb) / 255f + ((argb >> 16) & 0xFF) / 255f, + ((argb >> 8) & 0xFF) / 255f, + (argb & 0xFF) / 255f }; } @@ -155,7 +154,7 @@ public int getRgbColor(int index) { if (rgbColorTable == null || rgbColorTable.length == 0) { - return Color.BLACK; + return 0xFF000000; } index = Math.max(0, Math.min(index, actualMaxIndex)); return rgbColorTable[index]; diff --git a/library/src/test/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDIndexedTest.java b/library/src/test/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDIndexedTest.java new file mode 100644 index 00000000..96a474dc --- /dev/null +++ b/library/src/test/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDIndexedTest.java @@ -0,0 +1,142 @@ +/* + * 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 com.tom_roush.pdfbox.pdmodel.graphics.color; + +import java.io.IOException; +import java.io.OutputStream; + +import junit.framework.TestCase; + +import com.tom_roush.pdfbox.cos.COSArray; +import com.tom_roush.pdfbox.cos.COSInteger; +import com.tom_roush.pdfbox.cos.COSName; +import com.tom_roush.pdfbox.cos.COSStream; +import com.tom_roush.pdfbox.cos.COSString; + +/** + * Unit tests for PDIndexed color space. + */ +public class PDIndexedTest extends TestCase +{ + public void testDefaultConstructor() + { + PDIndexed indexed = new PDIndexed(); + assertEquals("Indexed", indexed.getName()); + assertEquals(1, indexed.getNumberOfComponents()); + float[] decode = indexed.getDefaultDecode(8); + assertEquals(0.0f, decode[0]); + assertEquals(255.0f, decode[1]); + } + + public void testRgbLookupFromString() throws IOException + { + // 3 entries: Red (255, 0, 0), Green (0, 255, 0), Blue (0, 0, 255) + byte[] lookup = new byte[] { + (byte) 255, 0, 0, + 0, (byte) 255, 0, + 0, 0, (byte) 255 + }; + + COSArray array = new COSArray(); + array.add(COSName.INDEXED); + array.add(COSName.DEVICERGB); + array.add(COSInteger.get(2)); // hival = 2 (3 colors: 0, 1, 2) + array.add(new COSString(lookup)); + + PDColorSpace cs = PDColorSpace.create(array); + assertTrue(cs instanceof PDIndexed); + + PDIndexed indexed = (PDIndexed) cs; + assertEquals(1, indexed.getNumberOfComponents()); + assertEquals(PDDeviceRGB.INSTANCE, indexed.getBaseColorSpace()); + + int[] rgbTable = indexed.getRgbColorTable(); + assertNotNull(rgbTable); + assertEquals(3, rgbTable.length); + + // Check packed ARGB values + assertEquals(0xFFFF0000, indexed.getRgbColor(0)); + assertEquals(0xFF00FF00, indexed.getRgbColor(1)); + assertEquals(0xFF0000FF, indexed.getRgbColor(2)); + + // Test toRGB + float[] rgb0 = indexed.toRGB(new float[] { 0 }); + assertEquals(1.0f, rgb0[0], 0.001f); + assertEquals(0.0f, rgb0[1], 0.001f); + assertEquals(0.0f, rgb0[2], 0.001f); + + float[] rgb1 = indexed.toRGB(new float[] { 1 }); + assertEquals(0.0f, rgb1[0], 0.001f); + assertEquals(1.0f, rgb1[1], 0.001f); + assertEquals(0.0f, rgb1[2], 0.001f); + + float[] rgb2 = indexed.toRGB(new float[] { 2 }); + assertEquals(0.0f, rgb2[0], 0.001f); + assertEquals(0.0f, rgb2[1], 0.001f); + assertEquals(1.0f, rgb2[2], 0.001f); + + // Test index clamping (out-of-bounds index should clamp safely) + assertEquals(0xFF0000FF, indexed.getRgbColor(10)); + assertEquals(0xFFFF0000, indexed.getRgbColor(-5)); + } + + public void testGrayLookupFromString() throws IOException + { + // 2 entries: Black (0), White (255) + byte[] lookup = new byte[] { 0, (byte) 255 }; + + COSArray array = new COSArray(); + array.add(COSName.INDEXED); + array.add(COSName.DEVICEGRAY); + array.add(COSInteger.get(1)); // hival = 1 (2 colors) + array.add(new COSString(lookup)); + + PDIndexed indexed = new PDIndexed(array); + assertEquals(2, indexed.getRgbColorTable().length); + + // Black (0) -> ARGB 0xFF000000 + assertEquals(0xFF000000, indexed.getRgbColor(0)); + // White (255) -> ARGB 0xFFFFFFFF + assertEquals(0xFFFFFFFF, indexed.getRgbColor(1)); + } + + public void testLookupFromStream() throws IOException + { + byte[] lookup = new byte[] { + (byte) 200, (byte) 100, 50 + }; + + COSStream stream = new COSStream(); + OutputStream out = stream.createOutputStream(); + out.write(lookup); + out.close(); + + COSArray array = new COSArray(); + array.add(COSName.INDEXED); + array.add(COSName.DEVICERGB); + array.add(COSInteger.get(0)); // hival = 0 (1 color) + array.add(stream); + + PDIndexed indexed = new PDIndexed(array); + assertEquals(1, indexed.getRgbColorTable().length); + + int color = indexed.getRgbColor(0); + assertEquals(200, (color >> 16) & 0xFF); + assertEquals(100, (color >> 8) & 0xFF); + assertEquals(50, color & 0xFF); + } +} From ffc1983abfd0571652907f893b392ca99867913d Mon Sep 17 00:00:00 2001 From: Sandeep Singh Date: Sat, 3 Oct 2026 11:33:19 +0530 Subject: [PATCH 3/6] Fix mask null safety, interpolation flag, resource inheritance, and 1-component color space fallbacks - PDImageXObject: pass interpolate directly to Bitmap.createScaledBitmap instead of inverting it - PDImageXObject: guard applyMask against null image to prevent NullPointerException - PDImageXObject: pass parent resources to getMask() and getSoftMask() instead of null - PDImageXObject: expose getResources() getter - PDColorSpace: fall back CALGRAY and SEPARATION to PDDeviceGray (1 component) instead of PDDeviceRGB (3 components) to prevent 3x vertical squashing and color noise - Add PDColorSpaceTest and PDImageXObjectTest unit test suites --- .../pdmodel/graphics/color/PDColorSpace.java | 10 +-- .../graphics/image/PDImageXObject.java | 21 +++-- .../graphics/color/PDColorSpaceTest.java | 69 ++++++++++++++++ .../graphics/image/PDImageXObjectTest.java | 81 +++++++++++++++++++ 4 files changed, 170 insertions(+), 11 deletions(-) create mode 100644 library/src/test/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDColorSpaceTest.java create mode 100644 library/src/test/java/com/tom_roush/pdfbox/pdmodel/graphics/image/PDImageXObjectTest.java diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDColorSpace.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDColorSpace.java index 090cec5b..c8e2fb24 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDColorSpace.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDColorSpace.java @@ -127,7 +127,7 @@ else if (name == COSName.DEVICERGB) { return PDDeviceRGB.INSTANCE; } - else if (name == COSName.DEVICEGRAY) + else if (name == COSName.DEVICEGRAY || name == COSName.CALGRAY) { return PDDeviceGray.INSTANCE; } @@ -169,8 +169,8 @@ else if (colorSpace instanceof COSArray) if (name == COSName.CALGRAY) { // return new PDCalGray(array); TODO: PdfBox-Android - Log.e("PdfBox-Android", "Unsupported color space kind: " + name + ". Will try DeviceRGB instead"); - return PDDeviceRGB.INSTANCE; + Log.w("PdfBox-Android", "Unsupported color space kind: " + name + ". Will try DeviceGray instead"); + return PDDeviceGray.INSTANCE; } else if (name == COSName.CALRGB) { @@ -191,8 +191,8 @@ else if (name == COSName.INDEXED) else if (name == COSName.SEPARATION) { // return new PDSeparation(array); - Log.e("PdfBox-Android", "Unsupported color space kind: " + name + ". Will try DeviceRGB instead"); - return PDDeviceRGB.INSTANCE; + Log.w("PdfBox-Android", "Unsupported color space kind: " + name + ". Will try DeviceGray instead"); + return PDDeviceGray.INSTANCE; } else if (name == COSName.ICCBASED) { diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/image/PDImageXObject.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/image/PDImageXObject.java index b6433f61..1a13180a 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/image/PDImageXObject.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/image/PDImageXObject.java @@ -628,10 +628,10 @@ public Bitmap getOpaqueImage(Rect region, int subsampling) throws IOException * @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 interpolateMask, + Bitmap applyMask(Bitmap image, Bitmap mask, boolean interpolateMask, boolean isSoft, float[] matte) { - if (mask == null) + if (image == null || mask == null) { return image; } @@ -732,9 +732,9 @@ private static int clampColor(int color) /** * High-quality image scaling. */ - private Bitmap scaleImage(Bitmap image, int width, int height, boolean interpolate) + Bitmap scaleImage(Bitmap image, int width, int height, boolean interpolate) { - return Bitmap.createScaledBitmap(image, width, height, !interpolate); + return Bitmap.createScaledBitmap(image, width, height, interpolate); } /** @@ -756,7 +756,7 @@ public PDImageXObject getMask() throws IOException if (cosStream != null) { // always DeviceGray - return new PDImageXObject(new PDStream(cosStream), null); + return new PDImageXObject(new PDStream(cosStream), resources); } return null; } @@ -787,7 +787,7 @@ public PDImageXObject getSoftMask() throws IOException if (cosStream != null) { // always DeviceGray - return new PDImageXObject(new PDStream(cosStream), null); + return new PDImageXObject(new PDStream(cosStream), resources); } return null; } @@ -857,6 +857,15 @@ else if (isStencil()) return colorSpace; } + /** + * Returns the resources associated with this image, or null if there are none. + * @return the resources + */ + public PDResources getResources() + { + return resources; + } + @Override public InputStream createInputStream() throws IOException { diff --git a/library/src/test/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDColorSpaceTest.java b/library/src/test/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDColorSpaceTest.java new file mode 100644 index 00000000..8e026ae3 --- /dev/null +++ b/library/src/test/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDColorSpaceTest.java @@ -0,0 +1,69 @@ +/* + * 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 com.tom_roush.pdfbox.pdmodel.graphics.color; + +import java.io.IOException; + +import junit.framework.TestCase; + +import com.tom_roush.pdfbox.cos.COSArray; +import com.tom_roush.pdfbox.cos.COSName; + +/** + * Unit tests for PDColorSpace creation and fallbacks. + */ +public class PDColorSpaceTest extends TestCase +{ + public void testBuiltInColorSpaces() throws IOException + { + assertEquals(PDDeviceRGB.INSTANCE, PDColorSpace.create(COSName.DEVICERGB)); + assertEquals(3, PDColorSpace.create(COSName.DEVICERGB).getNumberOfComponents()); + + assertEquals(PDDeviceCMYK.INSTANCE, PDColorSpace.create(COSName.DEVICECMYK)); + assertEquals(4, PDColorSpace.create(COSName.DEVICECMYK).getNumberOfComponents()); + + assertEquals(PDDeviceGray.INSTANCE, PDColorSpace.create(COSName.DEVICEGRAY)); + assertEquals(1, PDColorSpace.create(COSName.DEVICEGRAY).getNumberOfComponents()); + } + + public void testCalGrayFallbackPreservesSingleComponent() throws IOException + { + // CalGray as direct COSName + PDColorSpace direct = PDColorSpace.create(COSName.CALGRAY); + assertEquals(PDDeviceGray.INSTANCE, direct); + assertEquals(1, direct.getNumberOfComponents()); + + // CalGray as COSArray: [/CalGray <<...>>] + COSArray array = new COSArray(); + array.add(COSName.CALGRAY); + PDColorSpace fromArray = PDColorSpace.create(array); + assertEquals(PDDeviceGray.INSTANCE, fromArray); + assertEquals(1, fromArray.getNumberOfComponents()); + } + + public void testSeparationFallbackPreservesSingleComponent() throws IOException + { + // Separation as COSArray: [/Separation /Name /DeviceCMYK /Function] + COSArray array = new COSArray(); + array.add(COSName.SEPARATION); + array.add(COSName.getPDFName("Spot1")); + array.add(COSName.DEVICECMYK); + PDColorSpace cs = PDColorSpace.create(array); + assertEquals(PDDeviceGray.INSTANCE, cs); + assertEquals(1, cs.getNumberOfComponents()); + } +} diff --git a/library/src/test/java/com/tom_roush/pdfbox/pdmodel/graphics/image/PDImageXObjectTest.java b/library/src/test/java/com/tom_roush/pdfbox/pdmodel/graphics/image/PDImageXObjectTest.java new file mode 100644 index 00000000..5da51040 --- /dev/null +++ b/library/src/test/java/com/tom_roush/pdfbox/pdmodel/graphics/image/PDImageXObjectTest.java @@ -0,0 +1,81 @@ +/* + * 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 com.tom_roush.pdfbox.pdmodel.graphics.image; + +import android.graphics.Bitmap; + +import java.io.IOException; + +import junit.framework.TestCase; +import org.mockito.Mockito; + +import com.tom_roush.pdfbox.cos.COSName; +import com.tom_roush.pdfbox.cos.COSStream; +import com.tom_roush.pdfbox.pdmodel.PDDocument; +import com.tom_roush.pdfbox.pdmodel.PDResources; +import com.tom_roush.pdfbox.pdmodel.common.PDStream; + +/** + * Unit tests for PDImageXObject mask handling and resource inheritance. + */ +public class PDImageXObjectTest extends TestCase +{ + public void testApplyMaskNullSafety() throws IOException + { + PDDocument doc = new PDDocument(); + PDImageXObject imageXObject = new PDImageXObject(doc); + + Bitmap mockImage = Mockito.mock(Bitmap.class); + Bitmap mockMask = Mockito.mock(Bitmap.class); + + // When image is null, applyMask must return null without throwing NullPointerException + assertNull(imageXObject.applyMask(null, mockMask, false, false, null)); + assertNull(imageXObject.applyMask(null, null, false, false, null)); + + // When mask is null, applyMask must return the original image + assertSame(mockImage, imageXObject.applyMask(mockImage, null, false, false, null)); + + doc.close(); + } + + public void testMaskAndSoftMaskInheritResources() throws IOException + { + PDDocument doc = new PDDocument(); + PDResources resources = new PDResources(); + COSStream mainStream = new COSStream(); + + COSStream maskStream = new COSStream(); + mainStream.setItem(COSName.MASK, maskStream); + + COSStream smaskStream = new COSStream(); + mainStream.setItem(COSName.SMASK, smaskStream); + + PDStream pdStream = new PDStream(mainStream); + PDImageXObject image = new PDImageXObject(pdStream, resources); + assertSame(resources, image.getResources()); + + PDImageXObject mask = image.getMask(); + assertNotNull(mask); + assertSame("Mask must inherit parent image resources", resources, mask.getResources()); + + PDImageXObject smask = image.getSoftMask(); + assertNotNull(smask); + assertSame("Soft mask must inherit parent image resources", resources, smask.getResources()); + + doc.close(); + } +} From 00e99143815417e2d51b307dc119c77409a919fe Mon Sep 17 00:00:00 2001 From: Sandeep Singh Date: Sat, 3 Oct 2026 11:45:22 +0530 Subject: [PATCH 4/6] Harden SampledImageReader decoder and PDFBoxResourceLoader - SampledImageReader: guard JPEG fast-path with region == null, colorKey == null, and default decode check to prevent bypassing clipping, chroma masking, or decode arrays - SampledImageReader: validate dimensions and stream length in createBitmapFromRawStream, throwing descriptive IOException on truncated/empty streams or unsupported component counts - PDFBoxResourceLoader: throw descriptive IOException instead of throwing NullPointerException when uninitialized - Add SampledImageReaderTest and PDFBoxResourceLoaderTest unit tests --- .../pdfbox/android/PDFBoxResourceLoader.java | 3 +- .../graphics/image/SampledImageReader.java | 18 ++- .../android/PDFBoxResourceLoaderTest.java | 44 +++++++ .../image/SampledImageReaderTest.java | 107 ++++++++++++++++++ 4 files changed, 168 insertions(+), 4 deletions(-) create mode 100644 library/src/test/java/com/tom_roush/pdfbox/android/PDFBoxResourceLoaderTest.java create mode 100644 library/src/test/java/com/tom_roush/pdfbox/pdmodel/graphics/image/SampledImageReaderTest.java diff --git a/library/src/main/java/com/tom_roush/pdfbox/android/PDFBoxResourceLoader.java b/library/src/main/java/com/tom_roush/pdfbox/android/PDFBoxResourceLoader.java index f3c47123..db1e16dc 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/android/PDFBoxResourceLoader.java +++ b/library/src/main/java/com/tom_roush/pdfbox/android/PDFBoxResourceLoader.java @@ -63,8 +63,7 @@ public static InputStream getStream(String path) throws IOException { if (ASSET_MANAGER == null) { - Log.e("PdfBox-Android", - "PDFBoxResourceLoader is not initialized, call PDFBoxResourceLoader.init() before use"); + throw new IOException("PDFBoxResourceLoader is not initialized, call PDFBoxResourceLoader.init() before use"); } return ASSET_MANAGER.open(path); } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/image/SampledImageReader.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/image/SampledImageReader.java index 2aa1a14f..287fc9b3 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/image/SampledImageReader.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/image/SampledImageReader.java @@ -212,7 +212,8 @@ public static Bitmap getRGBImage(PDImage pdImage, Rect region, int subsampling, // in PDColorSpace#toRGBImage expects an 8-bit range, i.e. 0-255. final float[] defaultDecode = pdImage.getColorSpace().getDefaultDecode(8); final float[] decode = getDecodeArray(pdImage); - if (pdImage.getSuffix() != null && pdImage.getSuffix().equals("jpg") && subsampling == 1) + if (pdImage.getSuffix() != null && pdImage.getSuffix().equals("jpg") && subsampling == 1 + && region == null && colorKey == null && Arrays.equals(decode, defaultDecode)) { return BitmapFactory.decodeStream(pdImage.createInputStream()); } @@ -392,11 +393,20 @@ private static Bitmap from8bit(PDImage pdImage, Rect clipped, final int subsampl } } - private static Bitmap createBitmapFromRawStream(InputStream input, int originalWidth, int numComponents, + static Bitmap createBitmapFromRawStream(InputStream input, int originalWidth, int numComponents, int sampleSize, PDColorSpace colorSpace) throws IOException { + if (originalWidth <= 0 || numComponents <= 0) + { + throw new IOException("Invalid image dimensions or components: width=" + originalWidth + ", components=" + numComponents); + } + byte[] bytes = IOUtils.toByteArray(input); int originalHeight = bytes.length / numComponents / originalWidth; + if (originalHeight <= 0) + { + throw new IOException("Image stream contains insufficient data: " + bytes.length + " bytes for width=" + originalWidth + ", components=" + numComponents); + } if (colorSpace instanceof PDIndexed) { @@ -466,6 +476,10 @@ else if (numComponents == 4) } bytes = result; } + else + { + throw new IOException("Unsupported number of components for raw stream conversion: " + numComponents); + } Bitmap bitmap = Bitmap.createBitmap(originalWidth, originalHeight, Bitmap.Config.ARGB_8888); bitmap.copyPixelsFromBuffer(ByteBuffer.wrap(bytes)); if (sampleSize > 1) diff --git a/library/src/test/java/com/tom_roush/pdfbox/android/PDFBoxResourceLoaderTest.java b/library/src/test/java/com/tom_roush/pdfbox/android/PDFBoxResourceLoaderTest.java new file mode 100644 index 00000000..178056db --- /dev/null +++ b/library/src/test/java/com/tom_roush/pdfbox/android/PDFBoxResourceLoaderTest.java @@ -0,0 +1,44 @@ +/* + * 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 com.tom_roush.pdfbox.android; + +import java.io.IOException; + +import junit.framework.TestCase; + +/** + * Unit tests for PDFBoxResourceLoader initialization and error handling. + */ +public class PDFBoxResourceLoaderTest extends TestCase +{ + public void testGetStreamThrowsIOExceptionWhenUninitialized() + { + // When not initialized, getStream must throw IOException, not NullPointerException + if (!PDFBoxResourceLoader.isReady()) + { + try + { + PDFBoxResourceLoader.getStream("test/resource.dat"); + fail("Expected IOException when uninitialized"); + } + catch (IOException expected) + { + assertTrue(expected.getMessage().contains("PDFBoxResourceLoader is not initialized")); + } + } + } +} diff --git a/library/src/test/java/com/tom_roush/pdfbox/pdmodel/graphics/image/SampledImageReaderTest.java b/library/src/test/java/com/tom_roush/pdfbox/pdmodel/graphics/image/SampledImageReaderTest.java new file mode 100644 index 00000000..e2b76590 --- /dev/null +++ b/library/src/test/java/com/tom_roush/pdfbox/pdmodel/graphics/image/SampledImageReaderTest.java @@ -0,0 +1,107 @@ +/* + * 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 com.tom_roush.pdfbox.pdmodel.graphics.image; + +import java.io.ByteArrayInputStream; +import java.io.IOException; + +import junit.framework.TestCase; + +import com.tom_roush.pdfbox.pdmodel.graphics.color.PDDeviceRGB; + +/** + * Unit tests for SampledImageReader bounds checks and stream validation. + */ +public class SampledImageReaderTest extends TestCase +{ + public void testZeroWidthThrowsIOException() + { + byte[] data = new byte[] { 1, 2, 3 }; + try + { + SampledImageReader.createBitmapFromRawStream( + new ByteArrayInputStream(data), 0, 3, 1, PDDeviceRGB.INSTANCE); + fail("Expected IOException for zero width"); + } + catch (IOException expected) + { + assertTrue(expected.getMessage().contains("Invalid image dimensions")); + } + } + + public void testZeroComponentsThrowsIOException() + { + byte[] data = new byte[] { 1, 2, 3 }; + try + { + SampledImageReader.createBitmapFromRawStream( + new ByteArrayInputStream(data), 10, 0, 1, PDDeviceRGB.INSTANCE); + fail("Expected IOException for zero components"); + } + catch (IOException expected) + { + assertTrue(expected.getMessage().contains("Invalid image dimensions")); + } + } + + public void testInsufficientStreamDataThrowsIOException() + { + // 10 width * 3 components = 30 bytes required for a single row, but only 5 provided + byte[] truncated = new byte[5]; + try + { + SampledImageReader.createBitmapFromRawStream( + new ByteArrayInputStream(truncated), 10, 3, 1, PDDeviceRGB.INSTANCE); + fail("Expected IOException for insufficient stream data"); + } + catch (IOException expected) + { + assertTrue(expected.getMessage().contains("insufficient data")); + } + } + + public void testEmptyStreamThrowsIOException() + { + byte[] empty = new byte[0]; + try + { + SampledImageReader.createBitmapFromRawStream( + new ByteArrayInputStream(empty), 10, 3, 1, PDDeviceRGB.INSTANCE); + fail("Expected IOException for empty stream"); + } + catch (IOException expected) + { + assertTrue(expected.getMessage().contains("insufficient data")); + } + } + + public void testUnsupportedComponentsThrowsIOException() + { + // 5 components (not 1, 3, or 4 and not indexed) + byte[] data = new byte[100]; + try + { + SampledImageReader.createBitmapFromRawStream( + new ByteArrayInputStream(data), 2, 5, 1, PDDeviceRGB.INSTANCE); + fail("Expected IOException for unsupported components"); + } + catch (IOException expected) + { + assertTrue(expected.getMessage().contains("Unsupported number of components")); + } + } +} From dc19b8f49491fd2a94d7a2b00a8f0e3b8419875d Mon Sep 17 00:00:00 2001 From: Sandeep Singh Date: Sat, 3 Oct 2026 12:09:05 +0530 Subject: [PATCH 5/6] Optimize image pixel processing and detect JPEG SOF color spaces - JPEGFactory: inspect SOF marker in byte stream to accurately detect grayscale (PDDeviceGray) and CMYK (PDDeviceCMYK) JPEGs instead of unconditionally hardcoding PDDeviceRGB - JPEGFactory: batch alpha pixel extraction in chunked row buffers to prevent excessive heap allocations on large images - PDImageXObject: batch getPixels and setPixels in applyMask using chunked scanlines to slash row-by-row JNI crossings by >95% - PDDeviceGray: batch getPixels and setPixels in toRGBImage using chunked scanlines - PDDeviceCMYK: batch getPixels and setPixels in toRGBImage using chunked scanlines - LosslessFactory: batch getPixels in createFromRGBImage and createFromGrayImage using chunked scanlines - Add JPEGFactoryTest for grayscale, RGB, and CMYK SOF detection --- .../pdmodel/graphics/color/PDDeviceCMYK.java | 21 +-- .../pdmodel/graphics/color/PDDeviceGray.java | 24 ++-- .../pdmodel/graphics/image/JPEGFactory.java | 130 ++++++++++++++++-- .../graphics/image/LosslessFactory.java | 48 ++++--- .../graphics/image/PDImageXObject.java | 43 +++--- .../graphics/image/JPEGFactoryTest.java | 114 +++++++++++++++ 6 files changed, 314 insertions(+), 66 deletions(-) create mode 100644 library/src/test/java/com/tom_roush/pdfbox/pdmodel/graphics/image/JPEGFactoryTest.java diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDDeviceCMYK.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDDeviceCMYK.java index beeee231..b3bb8aed 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDDeviceCMYK.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDDeviceCMYK.java @@ -87,16 +87,21 @@ public Bitmap toRGBImage(Bitmap raster) throws IOException { int width = raster.getWidth(); int height = raster.getHeight(); - int[] imgPixels = new int[width]; + final int maxChunkPixels = 262144; + final int chunkRows = Math.min(height, Math.max(1, maxChunkPixels / width)); + final int bufferSize = chunkRows * width; + int[] imgPixels = new int[bufferSize]; + int[] outPixels = new int[bufferSize]; Bitmap image = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); - int[] outPixels = new int[width]; - for (int y = 0; y < height; y++) + for (int y = 0; y < height; y += chunkRows) { - raster.getPixels(imgPixels, 0, width, 0, y, width, 1); - for (int x = 0; x < width; x++) + int currentChunkRows = Math.min(chunkRows, height - y); + int currentPixelCount = currentChunkRows * width; + raster.getPixels(imgPixels, 0, width, 0, y, width, currentChunkRows); + for (int i = 0; i < currentPixelCount; i++) { - int pixel = imgPixels[x]; + int pixel = imgPixels[i]; float c = android.graphics.Color.red(pixel) / 255.0f; float m = android.graphics.Color.green(pixel) / 255.0f; float yVal = android.graphics.Color.blue(pixel) / 255.0f; @@ -104,9 +109,9 @@ public Bitmap toRGBImage(Bitmap raster) throws IOException int r = Math.round(255 * (1 - c) * (1 - k)); int g = Math.round(255 * (1 - m) * (1 - k)); int b = Math.round(255 * (1 - yVal) * (1 - k)); - outPixels[x] = android.graphics.Color.argb(255, r, g, b); + outPixels[i] = android.graphics.Color.argb(255, r, g, b); } - image.setPixels(outPixels, 0, width, 0, y, width, 1); + image.setPixels(outPixels, 0, width, 0, y, width, currentChunkRows); } return image; } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDDeviceGray.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDDeviceGray.java index 9e50eec1..af77927c 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDDeviceGray.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDDeviceGray.java @@ -81,23 +81,25 @@ public Bitmap toRGBImage(Bitmap raster) throws IOException int width = raster.getWidth(); int height = raster.getHeight(); - int[] imgPixels = new int[width]; + final int maxChunkPixels = 262144; + final int chunkRows = Math.min(height, Math.max(1, maxChunkPixels / width)); + final int bufferSize = chunkRows * width; + int[] imgPixels = new int[bufferSize]; + int[] outPixels = new int[bufferSize]; Bitmap image = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); - int[] outPixels = new int[width]; - int gray; - int rgb; - for (int y = 0; y < height; y++) + for (int y = 0; y < height; y += chunkRows) { - raster.getPixels(imgPixels, 0, width, 0, y, width, 1); - for (int x = 0; x < width; x++) + int currentChunkRows = Math.min(chunkRows, height - y); + int currentPixelCount = currentChunkRows * width; + raster.getPixels(imgPixels, 0, width, 0, y, width, currentChunkRows); + for (int i = 0; i < currentPixelCount; i++) { - gray = Color.alpha(imgPixels[x]); - rgb = Color.argb(255, gray, gray, gray); - outPixels[x] = rgb; + int gray = Color.alpha(imgPixels[i]); + outPixels[i] = Color.argb(255, gray, gray, gray); } - image.setPixels(outPixels, 0, width, 0, y, width, 1); + image.setPixels(outPixels, 0, width, 0, y, width, currentChunkRows); } return image; diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/image/JPEGFactory.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/image/JPEGFactory.java index 086c0411..4acb99f2 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/image/JPEGFactory.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/image/JPEGFactory.java @@ -21,6 +21,7 @@ import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; +import android.util.Log; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -35,6 +36,7 @@ import com.tom_roush.pdfbox.io.IOUtils; import com.tom_roush.pdfbox.pdmodel.PDDocument; import com.tom_roush.pdfbox.pdmodel.graphics.color.PDColorSpace; +import com.tom_roush.pdfbox.pdmodel.graphics.color.PDDeviceCMYK; import com.tom_roush.pdfbox.pdmodel.graphics.color.PDDeviceGray; import com.tom_roush.pdfbox.pdmodel.graphics.color.PDDeviceRGB; @@ -81,7 +83,19 @@ public static PDImageXObject createFromByteArray(PDDocument document, byte[] byt Dimensions meta = retrieveDimensions(byteStream); - PDColorSpace colorSpace = PDDeviceRGB.INSTANCE; // All images are RGB after being loaded by Bitmaps + PDColorSpace colorSpace; + if (meta.numComponents == 1) + { + colorSpace = PDDeviceGray.INSTANCE; + } + else if (meta.numComponents == 4) + { + colorSpace = PDDeviceCMYK.INSTANCE; + } + else + { + colorSpace = PDDeviceRGB.INSTANCE; + } // create PDImageXObject from stream PDImageXObject pdImage = new PDImageXObject(document, byteStream, @@ -107,10 +121,98 @@ private static Dimensions retrieveDimensions(ByteArrayInputStream stream) throws Dimensions meta = new Dimensions(); meta.width = options.outWidth; meta.height = options.outHeight; + parseJpegMetadata(stream, meta); return meta; } + private static void parseJpegMetadata(ByteArrayInputStream stream, Dimensions meta) + { + stream.mark(0); + meta.numComponents = 3; + try + { + if (stream.read() != 0xFF || stream.read() != 0xD8) + { + return; + } + int b; + while ((b = stream.read()) != -1) + { + if (b == 0xFF) + { + while ((b = stream.read()) == 0xFF) + { + // skip padding bytes + } + if (b == -1) + { + break; + } + + // Check SOF markers + if ((b >= 0xC0 && b <= 0xC3) || (b >= 0xC5 && b <= 0xC7) || (b >= 0xC9 && b <= 0xCB)) + { + // SOF marker: skip length (2 bytes) + stream.skip(2); + int precision = stream.read(); + int h1 = stream.read(); + int h2 = stream.read(); + int w1 = stream.read(); + int w2 = stream.read(); + int components = stream.read(); + if (components > 0) + { + meta.numComponents = components; + } + if (meta.height <= 0 && h1 != -1 && h2 != -1) + { + meta.height = (h1 << 8) | h2; + } + if (meta.width <= 0 && w1 != -1 && w2 != -1) + { + meta.width = (w1 << 8) | w2; + } + break; + } + else if (b == 0xDA || b == 0xD9) + { + // SOS (Start of Scan) or EOI + break; + } + else if (b >= 0xD0 && b <= 0xD7) + { + // RST markers have no payload + continue; + } + else + { + // Other markers: read 2-byte length and skip payload + int lenHigh = stream.read(); + int lenLow = stream.read(); + if (lenHigh == -1 || lenLow == -1) + { + break; + } + int length = (lenHigh << 8) | lenLow; + if (length > 2) + { + stream.skip(length - 2); + } + } + } + } + } + catch (Exception e) + { + Log.w("PdfBox-Android", "Could not parse JPEG components: " + e.getMessage()); + } + finally + { + stream.reset(); + } + } + /** * Creates a new JPEG PDImageXObject from a Bitmap. *

@@ -232,20 +334,28 @@ private static PDImageXObject createAlphaFromARGBImage(PDDocument document, Bitm return null; } - int[] pixels = new int[image.getHeight() * image.getWidth()]; - image.getPixels(pixels, 0, image.getWidth(), 0, 0, image.getWidth(), image.getHeight()); + final int width = image.getWidth(); + final int height = image.getHeight(); + final int maxChunkPixels = 262144; + final int chunkRows = Math.min(height, Math.max(1, maxChunkPixels / width)); + final int bufferSize = chunkRows * width; + int[] pixels = new int[bufferSize]; - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - int bpc; - bpc = 8; - for (int pixel : pixels) + ByteArrayOutputStream bos = new ByteArrayOutputStream(width * height); + int bpc = 8; + for (int y = 0; y < height; y += chunkRows) { - bos.write(Color.alpha(pixel)); + int currentChunkRows = Math.min(chunkRows, height - y); + int currentPixelCount = currentChunkRows * width; + image.getPixels(pixels, 0, width, 0, y, width, currentChunkRows); + for (int i = 0; i < currentPixelCount; i++) + { + bos.write((pixels[i] >> 24) & 0xFF); + } } - // } return prepareImageXObject(document, bos.toByteArray(), - image.getWidth(), image.getHeight(), bpc, PDDeviceGray.INSTANCE); + width, height, bpc, PDDeviceGray.INSTANCE); } /** diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/image/LosslessFactory.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/image/LosslessFactory.java index b5f0bbc3..ade81add 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/image/LosslessFactory.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/image/LosslessFactory.java @@ -123,22 +123,29 @@ private static PDImageXObject createFromGrayImage(Bitmap image, PDDocument docum { int height = image.getHeight(); int width = image.getWidth(); - int[] rgbLineBuffer = new int[width]; + final int maxChunkPixels = 262144; + final int chunkRows = Math.min(height, Math.max(1, maxChunkPixels / width)); + final int bufferSize = chunkRows * width; + int[] rgbLineBuffer = new int[bufferSize]; int bpc = 8; ByteArrayOutputStream baos = new ByteArrayOutputStream(((width*bpc/8)+(width*bpc%8 != 0 ? 1:0))*height); MemoryCacheImageOutputStream mcios = new MemoryCacheImageOutputStream(baos); - for (int y = 0; y < height; ++y) + for (int y = 0; y < height; y += chunkRows) { - image.getPixels(rgbLineBuffer, 0, width, 0, y, width, 1); - for (int pixel : rgbLineBuffer) + int currentChunkRows = Math.min(chunkRows, height - y); + image.getPixels(rgbLineBuffer, 0, width, 0, y, width, currentChunkRows); + for (int r = 0; r < currentChunkRows; r++) { - mcios.writeBits(pixel & 0xFF, bpc); - } - - int bitOffset = mcios.getBitOffset(); - if (bitOffset != 0) - { - mcios.writeBits(0, 8 - bitOffset); + int rowOffset = r * width; + for (int x = 0; x < width; x++) + { + mcios.writeBits(rgbLineBuffer[rowOffset + x] & 0xFF, bpc); + } + int bitOffset = mcios.getBitOffset(); + if (bitOffset != 0) + { + mcios.writeBits(0, 8 - bitOffset); + } } } mcios.flush(); @@ -151,7 +158,10 @@ private static PDImageXObject createFromRGBImage(Bitmap image, PDDocument docume { int height = image.getHeight(); int width = image.getWidth(); - int[] rgbLineBuffer = new int[width]; + final int maxChunkPixels = 262144; + final int chunkRows = Math.min(height, Math.max(1, maxChunkPixels / width)); + final int bufferSize = chunkRows * width; + int[] rgbLineBuffer = new int[bufferSize]; int bpc = 8; PDDeviceColorSpace deviceColorSpace = PDDeviceRGB.INSTANCE; byte[] imageData = new byte[width * height * 3]; @@ -168,20 +178,20 @@ private static PDImageXObject createFromRGBImage(Bitmap image, PDDocument docume { alphaImageData = new byte[0]; } - for (int y = 0; y < height; ++y) + for (int y = 0; y < height; y += chunkRows) { - image.getPixels(rgbLineBuffer, 0, width, 0, y, width, 1); - for (int pixel : rgbLineBuffer) + int currentChunkRows = Math.min(chunkRows, height - y); + int currentPixelCount = currentChunkRows * width; + image.getPixels(rgbLineBuffer, 0, width, 0, y, width, currentChunkRows); + for (int i = 0; i < currentPixelCount; i++) { + int pixel = rgbLineBuffer[i]; imageData[byteIdx++] = (byte) ((pixel >> 16) & 0xFF); imageData[byteIdx++] = (byte) ((pixel >> 8) & 0xFF); imageData[byteIdx++] = (byte) (pixel & 0xFF); if (image.hasAlpha()) { - { - // write a byte - alphaImageData[alphaByteIdx++] = (byte) ((pixel >> 24) & 0xFF); - } + alphaImageData[alphaByteIdx++] = (byte) ((pixel >> 24) & 0xFF); } } } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/image/PDImageXObject.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/image/PDImageXObject.java index 1a13180a..a15c668d 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/image/PDImageXObject.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/image/PDImageXObject.java @@ -654,26 +654,31 @@ Bitmap applyMask(Bitmap image, Bitmap mask, boolean interpolateMask, image = image.copy(Bitmap.Config.ARGB_8888, true); } final boolean isMaskAlpha8 = mask.getConfig() == Bitmap.Config.ALPHA_8; - int[] pixels = new int[width]; - int[] maskPixels = new int[width]; + final int maxChunkPixels = 262144; + final int chunkRows = Math.min(height, Math.max(1, maxChunkPixels / width)); + final int bufferSize = chunkRows * width; + int[] pixels = new int[bufferSize]; + int[] maskPixels = new int[bufferSize]; if (matte == null) { - for (int y = 0; y < height; y++) + for (int y = 0; y < height; y += chunkRows) { - 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++) + int currentChunkRows = Math.min(chunkRows, height - y); + int currentPixelCount = currentChunkRows * width; + image.getPixels(pixels, 0, width, 0, y, width, currentChunkRows); + mask.getPixels(maskPixels, 0, width, 0, y, width, currentChunkRows); + for (int i = 0; i < currentPixelCount; i++) { - int maskVal = maskPixels[x]; + int maskVal = maskPixels[i]; int a = isMaskAlpha8 ? ((maskVal >> 24) & 0xFF) : ((maskVal >> 16) & 0xFF); if (!isSoft) { a = 255 - a; } - pixels[x] = (pixels[x] & 0x00FFFFFF) | (a << 24); + pixels[i] = (pixels[i] & 0x00FFFFFF) | (a << 24); } - image.setPixels(pixels, 0, width, 0, y, width, 1); + image.setPixels(pixels, 0, width, 0, y, width, currentChunkRows); } } else @@ -696,29 +701,31 @@ Bitmap applyMask(Bitmap image, Bitmap mask, boolean interpolateMask, 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++) + for (int y = 0; y < height; y += chunkRows) { - 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++) + int currentChunkRows = Math.min(chunkRows, height - y); + int currentPixelCount = currentChunkRows * width; + image.getPixels(pixels, 0, width, 0, y, width, currentChunkRows); + mask.getPixels(maskPixels, 0, width, 0, y, width, currentChunkRows); + for (int i = 0; i < currentPixelCount; i++) { - int maskVal = maskPixels[x]; + int maskVal = maskPixels[i]; int a = isMaskAlpha8 ? ((maskVal >> 24) & 0xFF) : ((maskVal >> 16) & 0xFF); if (a == 0) { - pixels[x] = pixels[x] & 0x00FFFFFF; + pixels[i] = pixels[i] & 0x00FFFFFF; continue; } - int rgb = pixels[x]; + int rgb = pixels[i]; 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); + pixels[i] = Color.argb(a, r, g, b); } - image.setPixels(pixels, 0, width, 0, y, width, 1); + image.setPixels(pixels, 0, width, 0, y, width, currentChunkRows); } } return image; diff --git a/library/src/test/java/com/tom_roush/pdfbox/pdmodel/graphics/image/JPEGFactoryTest.java b/library/src/test/java/com/tom_roush/pdfbox/pdmodel/graphics/image/JPEGFactoryTest.java new file mode 100644 index 00000000..fe0dbbe5 --- /dev/null +++ b/library/src/test/java/com/tom_roush/pdfbox/pdmodel/graphics/image/JPEGFactoryTest.java @@ -0,0 +1,114 @@ +/* + * 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 com.tom_roush.pdfbox.pdmodel.graphics.image; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import junit.framework.TestCase; + +import com.tom_roush.pdfbox.pdmodel.PDDocument; +import com.tom_roush.pdfbox.pdmodel.graphics.color.PDDeviceCMYK; +import com.tom_roush.pdfbox.pdmodel.graphics.color.PDDeviceGray; +import com.tom_roush.pdfbox.pdmodel.graphics.color.PDDeviceRGB; + +/** + * Unit tests for JPEGFactory SOF component detection and color space selection. + */ +public class JPEGFactoryTest extends TestCase +{ + private byte[] createSyntheticJpeg(int width, int height, int numComponents) throws IOException + { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + // SOI + out.write(0xFF); + out.write(0xD8); + + // SOF0 (Baseline DCT) + out.write(0xFF); + out.write(0xC0); + + int length = 8 + (3 * numComponents); + out.write((length >> 8) & 0xFF); + out.write(length & 0xFF); + + out.write(8); // Precision: 8 bits + out.write((height >> 8) & 0xFF); + out.write(height & 0xFF); + out.write((width >> 8) & 0xFF); + out.write(width & 0xFF); + out.write(numComponents); + + for (int i = 1; i <= numComponents; i++) + { + out.write(i); // Component ID + out.write(0x11); // Sampling factors + out.write(0); // Quantization table number + } + + // EOI + out.write(0xFF); + out.write(0xD9); + + return out.toByteArray(); + } + + public void testGrayscaleJpegDetectedAsDeviceGray() throws IOException + { + PDDocument doc = new PDDocument(); + byte[] grayJpeg = createSyntheticJpeg(40, 25, 1); + + PDImageXObject image = JPEGFactory.createFromByteArray(doc, grayJpeg); + assertNotNull(image); + assertEquals(40, image.getWidth()); + assertEquals(25, image.getHeight()); + assertEquals(PDDeviceGray.INSTANCE, image.getColorSpace()); + assertEquals(1, image.getColorSpace().getNumberOfComponents()); + + doc.close(); + } + + public void testRgbJpegDetectedAsDeviceRGB() throws IOException + { + PDDocument doc = new PDDocument(); + byte[] rgbJpeg = createSyntheticJpeg(50, 30, 3); + + PDImageXObject image = JPEGFactory.createFromByteArray(doc, rgbJpeg); + assertNotNull(image); + assertEquals(50, image.getWidth()); + assertEquals(30, image.getHeight()); + assertEquals(PDDeviceRGB.INSTANCE, image.getColorSpace()); + assertEquals(3, image.getColorSpace().getNumberOfComponents()); + + doc.close(); + } + + public void testCmykJpegDetectedAsDeviceCMYK() throws IOException + { + PDDocument doc = new PDDocument(); + byte[] cmykJpeg = createSyntheticJpeg(60, 35, 4); + + PDImageXObject image = JPEGFactory.createFromByteArray(doc, cmykJpeg); + assertNotNull(image); + assertEquals(60, image.getWidth()); + assertEquals(35, image.getHeight()); + assertEquals(PDDeviceCMYK.INSTANCE, image.getColorSpace()); + assertEquals(4, image.getColorSpace().getNumberOfComponents()); + + doc.close(); + } +} From b8dead70cf3bbf8e75e49e80c7776056151fb79f Mon Sep 17 00:00:00 2001 From: Sandeep Singh Date: Sat, 3 Oct 2026 14:51:23 +0530 Subject: [PATCH 6/6] Updated dependencies and changed minsdk version to 23 --- README.md | 2 +- gradle.properties | 2 +- gradle/libs.versions.toml | 10 +++++----- library/build.gradle.kts | 2 +- sample/build.gradle.kts | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 190a7afe..0dc4c4d0 100644 --- a/README.md +++ b/README.md @@ -55,4 +55,4 @@ Important notes * Currently based on PDFBox v2.0.37 -* Requires API 19 or greater for full functionality +* Requires API 23 or greater for full functionality diff --git a/gradle.properties b/gradle.properties index 0734d19c..5762a13a 100755 --- a/gradle.properties +++ b/gradle.properties @@ -17,7 +17,7 @@ org.gradle.parallel=true VERSION_NAME=2.0.37.0-SNAPSHOT VERSION_CODE=1 -ANDROID_BUILD_MIN_SDK_VERSION=21 +ANDROID_BUILD_MIN_SDK_VERSION=23 ANDROID_BUILD_TARGET_SDK_VERSION=36 ANDROID_BUILD_SDK_VERSION=36 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 468e4048..d5b1e378 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,11 +1,11 @@ [versions] -agp = "8.7.3" -bouncycastle = "1.85" -appcompat = "1.7.0" +agp = "8.8.2" +bouncycastle = "1.86" +appcompat = "1.8.0" junit = "4.13.2" -mockito = "5.12.0" +mockito = "5.24.0" diffutils = "1.3.0" -testRunner = "1.6.2" +testRunner = "1.7.0" jp2Android = "1.0.5" [libraries] diff --git a/library/build.gradle.kts b/library/build.gradle.kts index eca85e6e..deae6378 100644 --- a/library/build.gradle.kts +++ b/library/build.gradle.kts @@ -16,7 +16,7 @@ android { compileSdk = (project.findProperty("ANDROID_BUILD_SDK_VERSION") as String?)?.toInt() ?: 37 defaultConfig { - minSdk = (project.findProperty("ANDROID_BUILD_MIN_SDK_VERSION") as String?)?.toInt() ?: 21 + minSdk = (project.findProperty("ANDROID_BUILD_MIN_SDK_VERSION") as String?)?.toInt() ?: 23 testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunnerArguments["notAnnotation"] = "androidx.test.filters.FlakyTest" consumerProguardFiles("consumer-proguard-rules.txt") diff --git a/sample/build.gradle.kts b/sample/build.gradle.kts index 96565084..97244d42 100644 --- a/sample/build.gradle.kts +++ b/sample/build.gradle.kts @@ -11,7 +11,7 @@ android { defaultConfig { applicationId = "com.tom_roush.pdfbox.sample" - minSdk = (project.findProperty("ANDROID_BUILD_MIN_SDK_VERSION") as String?)?.toInt() ?: 21 + minSdk = (project.findProperty("ANDROID_BUILD_MIN_SDK_VERSION") as String?)?.toInt() ?: 23 targetSdk = (project.findProperty("ANDROID_BUILD_TARGET_SDK_VERSION") as String?)?.toInt() ?: 37 versionName = versionNameProp versionCode = versionCodeProp