diff --git a/library/src/androidTest/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/ShadingEnd2EndTest.java b/library/src/androidTest/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/ShadingEnd2EndTest.java new file mode 100644 index 00000000..7b8dc5a7 --- /dev/null +++ b/library/src/androidTest/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/ShadingEnd2EndTest.java @@ -0,0 +1,301 @@ +package com.tom_roush.pdfbox.pdmodel.graphics.shading; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.util.Log; + +import androidx.test.platform.app.InstrumentationRegistry; + +import com.tom_roush.harmony.javax.imageio.stream.MemoryCacheImageOutputStream; +import com.tom_roush.pdfbox.android.PDFBoxResourceLoader; +import com.tom_roush.pdfbox.cos.COSArray; +import com.tom_roush.pdfbox.cos.COSDictionary; +import com.tom_roush.pdfbox.cos.COSFloat; +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.pdmodel.PDDocument; +import com.tom_roush.pdfbox.pdmodel.PDPage; +import com.tom_roush.pdfbox.pdmodel.PDPageContentStream; +import com.tom_roush.pdfbox.pdmodel.common.function.PDFunctionType2; +import com.tom_roush.pdfbox.pdmodel.graphics.color.PDDeviceRGB; +import com.tom_roush.pdfbox.rendering.ImageType; +import com.tom_roush.pdfbox.rendering.PDFRenderer; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; + +public class ShadingEnd2EndTest { + + private Context testContext; + + @Before + public void setUp() { + testContext = InstrumentationRegistry.getInstrumentation().getContext(); + PDFBoxResourceLoader.init(testContext); + } + + @Test + public void TestShading2() { + try { + Bitmap bitmap = getShadingBitmap(2); + int height = bitmap.getHeight(); + Assert.assertEquals("(5, -5) should be white", Color.WHITE, bitmap.getPixel(5, height - 5)); + int cdiff = calcColorDiff(Color.RED, bitmap.getPixel(13, height - 13)); + Assert.assertTrue("(13, -13) should be red, diff:" + cdiff, cdiff < 15); + cdiff = calcColorDiff(Color.GREEN, bitmap.getPixel(95, height - 95)); + Assert.assertTrue("(95, -95) should be green, diff:" + cdiff, cdiff < 35); + Assert.assertEquals("(110, -110) should be white", Color.WHITE, bitmap.getPixel(110, height - 110)); + } + catch (IOException e){ + Assert.fail("Exception: " + e.getMessage()); + } + } + + @Test + public void TestShading3(){ + try { + Bitmap bitmap = getShadingBitmap(3); + int cdiff = calcColorDiff(Color.rgb(10, 10, 10), bitmap.getPixel(100, 394)); + Assert.assertTrue("(100, 394) should be black, diff: " + cdiff, cdiff < 20); + + cdiff = calcColorDiff(Color.rgb(128, 128, 128), bitmap.getPixel(102, 367)); + Assert.assertTrue("(102, 367) should be gray, diff: " + cdiff, cdiff < 20); + + cdiff = calcColorDiff(Color.rgb(255, 255, 255), bitmap.getPixel(102, 340)); + Assert.assertTrue("(102, 340) should be white, diff: " + cdiff, cdiff < 20); + + cdiff = calcColorDiff(Color.rgb(153, 153, 153), bitmap.getPixel(130, 390)); + Assert.assertTrue("(130, 390) should be gray, diff: " + cdiff, cdiff < 20); + } + catch (IOException e){ + Assert.fail("Exception: " + e.getMessage()); + } + } + + @Test + public void TestShading4(){ + try { + Bitmap bitmap = getShadingBitmap(4); + int height = bitmap.getHeight(); + int cdiff = calcColorDiff(Color.RED, bitmap.getPixel(5, height - 2)); + Assert.assertTrue("(5, -2) should be red, diff: " + cdiff, cdiff < 20); + + cdiff = calcColorDiff(Color.GREEN, bitmap.getPixel(100, height - 97)); + Assert.assertTrue("(100, -97) should be green, diff: " + cdiff, cdiff < 25); + + cdiff = calcColorDiff(Color.BLUE, bitmap.getPixel(195, height - 2)); + Assert.assertTrue("(195, -2) should be green, diff: " + cdiff, cdiff < 25); + + cdiff = calcColorDiff(Color.WHITE, bitmap.getPixel(5, height - 8)); + Assert.assertTrue("(5, -8) should be white, diff: " + cdiff, cdiff < 20); + + cdiff = calcColorDiff(Color.WHITE, bitmap.getPixel(195, height - 10)); + Assert.assertTrue("(195, -10) should be white, diff: " + cdiff, cdiff < 20); + } + catch (IOException e){ + Assert.fail("Exception: " + e.getMessage()); + } + } + + private static int calcColorDiff(int a, int cb) { + int r = Color.red(cb); + int g = Color.green(cb); + int b = Color.blue(cb); + Log.i("Test", String.format("(%d, %d, %d)", r, g, b)); + return Math.abs(Color.red(a) - r) + + Math.abs(Color.green(a) - g) + + Math.abs(Color.blue(a) - b); + } + + public static Bitmap getShadingBitmap(int stype) throws IOException { + PDShading shading; + if(stype == 2){ + shading = createShading2(); + } + else if(stype == 3){ + shading = createShading3(); + } + else if(stype == 4){ + shading = createShading4(); + } + else{ + throw new UnsupportedOperationException("shading type not support:" + stype); + } + PDDocument document = new PDDocument(); + PDPage page = new PDPage(); + document.addPage(page); + try (PDPageContentStream contentStream = new PDPageContentStream(document, page, PDPageContentStream.AppendMode.APPEND, false)) + { + contentStream.shadingFill(shading); + } + PDFRenderer renderer = new PDFRenderer(document); + // Render the image to an RGB Bitmap + return renderer.renderImage(0, 1, ImageType.RGB); + } + + + private static PDShadingType4 createShading4() throws IOException { + // See PDF 32000 specification, +// 8.7.4.5.5 Type 4 Shadings (Free-Form Gouraud-Shaded Triangle Meshes) + PDShadingType4 gouraudShading = new PDShadingType4(new COSStream()); + gouraudShading.setShadingType(PDShading.SHADING_TYPE4); +// we use multiple of 8, so that no padding is needed + gouraudShading.setBitsPerFlag(8); + gouraudShading.setBitsPerCoordinate(16); + gouraudShading.setBitsPerComponent(8); + COSArray decodeArray = new COSArray(); +// coordinates x y map 16 bits 0..FFFF to 0..FFFF to make your life easy +// so no calculation is needed, but you can only use integer coordinates +// for real numbers, you'll need smaller bounds, e.g. 0xFFFF / 0xA = 0x1999 +// would allow 1 point decimal result coordinate. +// See in PDF specification: 8.9.5.2 Decode Arrays + decodeArray.add(COSInteger.ZERO); + decodeArray.add(COSInteger.get(0xFFFF)); + decodeArray.add(COSInteger.ZERO); + decodeArray.add(COSInteger.get(0xFFFF)); +// colors r g b map 8 bits from 0..FF to 0..1 + decodeArray.add(COSInteger.ZERO); + decodeArray.add(COSInteger.ONE); + decodeArray.add(COSInteger.ZERO); + decodeArray.add(COSInteger.ONE); + decodeArray.add(COSInteger.ZERO); + decodeArray.add(COSInteger.ONE); + gouraudShading.setDecodeValues(decodeArray); + gouraudShading.setColorSpace(PDDeviceRGB.INSTANCE); + +// Function is not required for type 4 shadings and not really useful, +// because if a function would be used, each edge "color" of a triangle would be one value, +// which would then transformed into n color components by the function so it is +// difficult to get 3 "extremes". + + OutputStream os = ((COSStream) gouraudShading.getCOSObject()).createOutputStream(); + MemoryCacheImageOutputStream mcos = new MemoryCacheImageOutputStream(os); + +// Vertex 1, starts with flag1 +// (flags always 0 for vertices of start triangle) + mcos.writeByte(0); +// x1 y1 (left corner) + mcos.writeShort(0); + mcos.writeShort(0); +// r1 g1 b1 (red) + mcos.writeByte(0xFF); + mcos.writeByte(0); + mcos.writeByte(0); + +// Vertex 2, starts with flag2 + mcos.writeByte(0); +// x2 y2 (top corner) + mcos.writeShort(100); + mcos.writeShort(100); +// r2 g2 b2 (green) + mcos.writeByte(0); + mcos.writeByte(0xFF); + mcos.writeByte(0); + +// Vertex 3, starts with flag3 + mcos.writeByte(0); +// x3 y3 (right corner) + mcos.writeShort(200); + mcos.writeShort(0); +// r3 g3 b3 (blue) + mcos.writeByte(0); + mcos.writeByte(0); + mcos.writeByte(0xFF); + + mcos.close(); +// outside stream MUST be closed as well, see javadoc of MemoryCacheImageOutputStream + os.close(); + return gouraudShading; + } + + private static PDShadingType3 createShading3() throws IOException { +// type 2 (exponential) function with attributes + COSDictionary fdict = new COSDictionary(); + fdict.setInt(COSName.FUNCTION_TYPE, 2); + COSArray domain = new COSArray(); + domain.add(COSInteger.get(0)); + domain.add(COSInteger.get(1)); + COSArray c0 = new COSArray(); + c0.add(COSFloat.get("1")); + c0.add(COSFloat.get("1")); + c0.add(COSFloat.get("1")); + COSArray c1 = new COSArray(); + c1.add(COSFloat.get("0")); + c1.add(COSFloat.get("0")); + c1.add(COSFloat.get("0")); + fdict.setItem(COSName.DOMAIN, domain); + fdict.setItem(COSName.C0, c0); + fdict.setItem(COSName.C1, c1); + fdict.setInt(COSName.N, 1); + PDFunctionType2 func = new PDFunctionType2(fdict); + +// radial shading with attributes + PDShadingType3 radialShading = new PDShadingType3(new COSDictionary()); + radialShading.setColorSpace(PDDeviceRGB.INSTANCE); + radialShading.setShadingType(PDShading.SHADING_TYPE3); + COSArray coords2 = new COSArray(); + coords2.add(COSInteger.get(100)); + coords2.add(COSInteger.get(400)); + coords2.add(COSInteger.get(50)); // radius1 + coords2.add(COSInteger.get(100)); + coords2.add(COSInteger.get(400)); + coords2.add(COSInteger.get(0)); // radius2 + radialShading.setCoords(coords2); + radialShading.setFunction(func); + + return radialShading; + } + + private static PDShadingType2 createShading2() throws IOException { + int startColor = Color.RED; + int endColor = Color.GREEN; + + COSDictionary fdict = new COSDictionary(); + + fdict.setInt(COSName.FUNCTION_TYPE, 2); + + COSArray domain = new COSArray(); + domain.add(COSInteger.get(0)); + domain.add(COSInteger.get(1)); + + COSArray c0 = new COSArray(); + c0.add(new COSFloat(Color.red(startColor) / 255f)); + c0.add(new COSFloat(Color.green(startColor) / 255f)); + c0.add(new COSFloat(Color.blue(startColor) / 255f)); + + COSArray c1 = new COSArray(); + c1.add(new COSFloat(Color.red(endColor) / 255f)); + c1.add(new COSFloat(Color.green(endColor) / 255f)); + c1.add(new COSFloat(Color.blue(endColor) / 255f)); + + fdict.setItem(COSName.DOMAIN, domain); + fdict.setItem(COSName.C0, c0); + fdict.setItem(COSName.C1, c1); + fdict.setInt(COSName.N, 1); + + PDFunctionType2 func = new PDFunctionType2(fdict); + + PDShadingType2 axialShading = new PDShadingType2(new COSDictionary()); + + axialShading.setColorSpace(PDDeviceRGB.INSTANCE); + axialShading.setShadingType(PDShading.SHADING_TYPE2); + + COSArray coords1 = new COSArray(); + coords1.add(new COSFloat(10)); + coords1.add(new COSFloat(10)); + coords1.add(new COSFloat(100)); + coords1.add(new COSFloat(100)); + + axialShading.setCoords(coords1); + axialShading.setFunction(func); + + return axialShading; + } +} diff --git a/library/src/main/java/com/tom_roush/harmony/awt/geom/AffineTransform.java b/library/src/main/java/com/tom_roush/harmony/awt/geom/AffineTransform.java index 18617f32..77e61b6a 100644 --- a/library/src/main/java/com/tom_roush/harmony/awt/geom/AffineTransform.java +++ b/library/src/main/java/com/tom_roush/harmony/awt/geom/AffineTransform.java @@ -19,6 +19,7 @@ */ package com.tom_roush.harmony.awt.geom; +import android.graphics.Path; import android.graphics.PointF; import java.io.IOException; @@ -223,6 +224,54 @@ else if (m01 != 0.0 || m10 != 0.0) return type; } + + /** + * Returns the x-scaling factor of this matrix. This is calculated from the scale and shear. + * + * @return The x-scaling factor. + */ + public double getScalingFactorX() + { + /** + * BM: if the trm is rotated, the calculation is a little more complicated + * + * The rotation matrix multiplied with the scaling matrix is: + * ( x 0 0) ( cos sin 0) ( x*cos x*sin 0) + * ( 0 y 0) * (-sin cos 0) = (-y*sin y*cos 0) + * ( 0 0 1) ( 0 0 1) ( 0 0 1) + * + * So, if you want to deduce x from the matrix you take + * M(0,0) = x*cos and M(0,1) = x*sin and use the theorem of Pythagoras + * + * sqrt(M(0,0)^2+M(0,1)^2) = + * sqrt(x2*cos2+x2*sin2) = + * sqrt(x2*(cos2+sin2)) = <- here is the trick cos2+sin2 is one + * sqrt(x2) = + * abs(x) + */ + if (m10 != 0.0f) + { + return (float) Math.sqrt(Math.pow(m00, 2) + + Math.pow(m10, 2)); + } + return m00; + } + + /** + * Returns the y-scaling factor of this matrix. This is calculated from the scale and shear. + * + * @return The y-scaling factor. + */ + public double getScalingFactorY() + { + if (m01 != 0.0f) + { + return (float) Math.sqrt(Math.pow(m01, 2) + + Math.pow(m11, 2)); + } + return m11; + } + public double getScaleX() { return m00; @@ -626,21 +675,16 @@ public void inverseTransform(double[] src, int srcOff, double[] dst, int dstOff, } } -// public Shape createTransformedShape(Shape src) -// { -// if (src == null) -// { -// return null; -// } -// if (src instanceof GeneralPath) -// { -// return ((GeneralPath) src).createTransformedShape(this); -// } -// PathIterator path = src.getPathIterator(this); -// GeneralPath dst = new GeneralPath(path.getWindingRule()); -// dst.append(path, false); -// return dst; -// } TODO: PdfBox-Android + public Path createTransformedShape(Path src) + { + if (src == null) + { + return null; + } + Path outPath = new Path(); + src.transform(this.toMatrix(), outPath); + return outPath; + } @Override public String toString() diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDPattern.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDPattern.java new file mode 100644 index 00000000..8bf66215 --- /dev/null +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDPattern.java @@ -0,0 +1,121 @@ +package com.tom_roush.pdfbox.pdmodel.graphics.color; + +import android.graphics.Bitmap; + +import com.tom_roush.pdfbox.cos.COSArray; +import com.tom_roush.pdfbox.cos.COSName; +import com.tom_roush.pdfbox.pdmodel.PDResources; +import com.tom_roush.pdfbox.pdmodel.graphics.pattern.PDAbstractPattern; + +import java.io.IOException; + +public final class PDPattern extends PDSpecialColorSpace { + /** A pattern which leaves no marks on the page. */ + private static PDColor EMPTY_PATTERN = new PDColor(new float[] { }, null); + + private final PDResources resources; + private PDColorSpace underlyingColorSpace; + + /** + * Creates a new pattern color space. + * + * @param resources The current resources. + */ + public PDPattern(PDResources resources) + { + this.resources = resources; + array = new COSArray(); + array.add(COSName.PATTERN); + } + + /** + * Creates a new uncolored tiling pattern color space. + * + * @param resources The current resources. + * @param colorSpace The underlying color space. + */ + public PDPattern(PDResources resources, PDColorSpace colorSpace) + { + this.resources = resources; + this.underlyingColorSpace = colorSpace; + array = new COSArray(); + array.add(COSName.PATTERN); + array.add(colorSpace); + } + + @Override + public String getName() + { + return COSName.PATTERN.getName(); + } + + @Override + public int getNumberOfComponents() + { + throw new UnsupportedOperationException(); + } + + @Override + public float[] getDefaultDecode(int bitsPerComponent) + { + throw new UnsupportedOperationException(); + } + + @Override + public PDColor getInitialColor() + { + return EMPTY_PATTERN; + } + + @Override + public float[] toRGB(float[] value) + { + throw new UnsupportedOperationException(); + } + + @Override + public Bitmap toRGBImage(Bitmap raster) throws IOException + { + throw new UnsupportedOperationException(); + } + +// @Override +// public Bitmap toRawImage(Bitmap raster) throws IOException +// { +// throw new UnsupportedOperationException(); +// } TODO: PdfBox-Android + + /** + * Returns the pattern for the given color. + * + * @param color color containing a pattern name + * @return pattern for the given color + * @throws java.io.IOException if the pattern name was not found. + */ + public PDAbstractPattern getPattern(PDColor color) throws IOException + { + PDAbstractPattern pattern = resources.getPattern(color.getPatternName()); + if (pattern == null) + { + throw new IOException("pattern " + color.getPatternName() + " was not found"); + } + else + { + return pattern; + } + } + + /** + * Returns the underlying color space, if this is an uncolored tiling pattern, otherwise null. + */ + public PDColorSpace getUnderlyingColorSpace() + { + return underlyingColorSpace; + } + + @Override + public String toString() + { + return "Pattern"; + } +} diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDSpecialColorSpace.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDSpecialColorSpace.java new file mode 100644 index 00000000..eecefe92 --- /dev/null +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDSpecialColorSpace.java @@ -0,0 +1,12 @@ +package com.tom_roush.pdfbox.pdmodel.graphics.color; + +import com.tom_roush.pdfbox.cos.COSBase; + +public abstract class PDSpecialColorSpace extends PDColorSpace { + + @Override + public COSBase getCOSObject() + { + return array; + } +} 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 c5652e97..16927e97 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 @@ -556,77 +556,120 @@ 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) + /** + * explicit mask: RGB + Binary -> ARGB + * soft mask: RGB + Gray -> ARGB + * + * @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; } + final int width = Math.max(image.getWidth(), mask.getWidth()); + final int height = Math.max(image.getHeight(), mask.getHeight()); - int width = image.getWidth(); - int height = image.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); } - else 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); } - - // compose to ARGB - Bitmap masked = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); - int[] outPixels = new int[width]; - - int rgb; - int alphaPixel; - int alpha; - int[] imgPixels = new int[width]; + 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]; - for (int y = 0; y < height; y++) - { - image.getPixels(imgPixels, 0, width, 0, y, width, 1); - mask.getPixels(maskPixels, 0, width, 0, y, width, 1); - for (int x = 0; x < width; x++) - { - rgb = imgPixels[x]; - int r = Color.red(rgb); - int g = Color.green(rgb); - int b = Color.blue(rgb); - alphaPixel = maskPixels[x]; - if (isSoft) - { - alpha = Color.alpha(alphaPixel); - if (matte != null && Float.compare(alpha, 0) != 0) - { - r = clampColor(((r / 255F - matte[0]) / (alpha / 255F) + matte[0]) * 255); - g = clampColor(((g / 255F - matte[1]) / (alpha / 255F) + matte[1]) * 255); - b = clampColor(((b / 255F - matte[2]) / (alpha / 255F) + matte[2]) * 255); + + // 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){ + 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) { + maskPixels[x] ^= -1; } + pixels[x] = pixels[x] & 0xffffff | maskPixels[x] & 0xff000000; } - else - { - alpha = 255 - Color.alpha(alphaPixel); + 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++) { + 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); } - - outPixels[x] = Color.argb(alpha, r, g, b); + image.setPixels(pixels, 0, width, 0, y, width, 1); } - masked.setPixels(outPixels, 0, width, 0, y, width, 1); } - - return masked; + return image; } - private int clampColor(float color) - { - return Float.valueOf(color < 0 ? 0 : (color > 255 ? 255 : color)).intValue(); + private int clampColor(float 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; } /** 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 77716133..747bf9ca 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 @@ -346,6 +346,17 @@ private static Bitmap from1Bit(PDImage pdImage, Rect clipped, final int subsampl } // faster, 8-bit non-decoded, non-colormasked image conversion + + /** + * + * @param pdImage + * @param clipped + * @param subsampling width of output bitmap + * @param width width of output bitmap + * @param height height of output bitmap + * @return + * @throws IOException + */ private static Bitmap from8bit(PDImage pdImage, Rect clipped, final int subsampling, final int width, final int height) throws IOException { @@ -356,8 +367,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()) @@ -380,44 +391,19 @@ 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) { - input.read(tempBytes); - if (y < starty || y % currentSubsampling > 0) - { - continue; - } - - for (int x = startx; x < startx + scanWidth; x += currentSubsampling) - { - 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; + // we just need to copy all sample data, then convert to RGB image. + return createBitmapFromRAW(input, inputWidth, numComponents, currentSubsampling); + } + else { + Bitmap origin = createBitmapFromRAW(input, inputWidth, numComponents, currentSubsampling); + if (currentSubsampling > 1) { + 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 { @@ -425,6 +411,42 @@ else if (numComponents == 1) } } + private static Bitmap createBitmapFromRAW(InputStream input, int orgWidth, int numComponents, int sampleSize) throws IOException { + byte[] bytes = IOUtils.toByteArray(input); + int orgHeight = bytes.length / numComponents / orgWidth; + if(numComponents == 1){ + byte[] result = new byte[orgWidth * orgHeight * 4]; + for (int i = orgWidth * orgHeight - 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[orgWidth * orgHeight * 4]; + for (int i = orgWidth * orgHeight - 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(orgWidth, orgHeight, Bitmap.Config.ARGB_8888); + bitmap.copyPixelsFromBuffer(ByteBuffer.wrap(bytes)); + if(sampleSize > 1){ + int width = orgWidth / sampleSize; + int height = orgHeight / 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 diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/AxialShadingContext.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/AxialShadingContext.java new file mode 100644 index 00000000..1b523d54 --- /dev/null +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/AxialShadingContext.java @@ -0,0 +1,227 @@ +package com.tom_roush.pdfbox.pdmodel.graphics.shading; + +import android.graphics.Bitmap; +import android.graphics.Color; +import android.graphics.Rect; +import android.util.Log; + +import com.tom_roush.harmony.awt.geom.AffineTransform; +import com.tom_roush.pdfbox.cos.COSArray; +import com.tom_roush.pdfbox.cos.COSBoolean; +import com.tom_roush.pdfbox.rendering.PaintContext; +import com.tom_roush.pdfbox.util.Matrix; + +import java.io.IOException; + +public class AxialShadingContext extends ShadingContext implements PaintContext { + + private PDShadingType2 axialShadingType; + + private final float[] coords; + private final float[] domain; + private final boolean[] extend; + private final double x1x0; + private final double y1y0; + private final float d1d0; + private final double denom; + + private final int factor; + private final int[] colorTable; + + private AffineTransform rat; + + /** + * Constructor creates an instance to be used for fill operations. + * + * @param shading the shading type to be used + * @param xform transformation for user to device space + * @param matrix the pattern matrix concatenated with that of the parent content stream + * @param deviceBounds the bounds of the area to paint, in device units + * @throws IOException if there is an error getting the color space or doing color conversion. + */ + public AxialShadingContext(PDShadingType2 shading, AffineTransform xform, + Matrix matrix, Rect deviceBounds) throws IOException + { + super(shading, xform, matrix); + this.axialShadingType = shading; + coords = shading.getCoords().toFloatArray(); + + // domain values + if (shading.getDomain() != null) + { + domain = shading.getDomain().toFloatArray(); + } + else + { + // set default values + domain = new float[] { 0, 1 }; + } + // extend values + COSArray extendValues = shading.getExtend(); + if (extendValues != null) + { + extend = new boolean[2]; + extend[0] = ((COSBoolean) extendValues.getObject(0)).getValue(); + extend[1] = ((COSBoolean) extendValues.getObject(1)).getValue(); + } + else + { + // set default values + extend = new boolean[] { false, false }; + } + // calculate some constants to be used in getRaster + x1x0 = coords[2] - coords[0]; + y1y0 = coords[3] - coords[1]; + d1d0 = domain[1] - domain[0]; + denom = Math.pow(x1x0, 2) + Math.pow(y1y0, 2); + + try + { + // get inverse transform to be independent of current user / device space + // when handling actual pixels in getRaster() + rat = matrix.createAffineTransform().createInverse(); + rat.concatenate(xform.createInverse()); + } + catch (AffineTransform.NoninvertibleTransformException ex) + { + Log.e("Pdfbox-Android", ex.getMessage() + ", matrix: " + matrix, ex); + rat = new AffineTransform(); + } + + // shading space -> device space + AffineTransform shadingToDevice = (AffineTransform)xform.clone(); + shadingToDevice.concatenate(matrix.createAffineTransform()); + + // worst case for the number of steps is opposite diagonal corners, so use that + double dist = Math.sqrt(Math.pow(deviceBounds.right - deviceBounds.left, 2) + + Math.pow(deviceBounds.bottom - deviceBounds.top, 2)); + factor = (int) Math.ceil(dist); + + // build the color table for the given number of steps + colorTable = calcColorTable(); + } + + /** + * Calculate the color on the axial line and store them in an array. + * + * @return an array, index denotes the relative position, the corresponding + * value is the color on the axial line + * @throws IOException if the color conversion fails. + */ + private int[] calcColorTable() throws IOException + { + int[] map = new int[factor + 1]; + if (factor == 0 || d1d0 == 0) + { + float[] values = axialShadingType.evalFunction(domain[0]); + map[0] = convertToRGB(values); + } + else + { + for (int i = 0; i <= factor; i++) + { + float t = domain[0] + d1d0 * i / factor; + float[] values = axialShadingType.evalFunction(t); + map[i] = convertToRGB(values); + } + } + return map; + } + + @Override + public void dispose() { + axialShadingType = null; + } + + @Override + public Bitmap.Config getColorModel() { + return super.getColorModel(); + } + + @Override + public Bitmap getRaster(int x, int y, int w, int h) { + // create writable raster + Bitmap raster = Bitmap.createBitmap(w, h, getColorModel()); + int[] data = new int[w * h]; + boolean useBackground; + float[] values = new float[2]; + for (int j = 0; j < h; j++) + { + for (int i = 0; i < w; i++) + { + useBackground = false; + values[0] = x + i; + values[1] = y + j; + rat.transform(values, 0, values, 0, 1); + double inputValue = x1x0 * (values[0] - coords[0]) + y1y0 * (values[1] - coords[1]); + // TODO this happens if start == end, see PDFBOX-1442 + if (denom == 0) + { + if (getBackground() == null) + { + continue; + } + useBackground = true; + } + else + { + inputValue /= denom; + } + // input value is out of range + if (inputValue < 0) + { + // the shading has to be extended if extend[0] == true + if (extend[0]) + { + inputValue = domain[0]; + } + else + { + if (getBackground() == null) + { + continue; + } + useBackground = true; + } + } + // input value is out of range + else if (inputValue > 1) + { + // the shading has to be extended if extend[1] == true + if (extend[1]) + { + inputValue = domain[1]; + } + else + { + if (getBackground() == null) + { + continue; + } + useBackground = true; + } + } + int value; + if (useBackground) + { + // use the given background color values + value = getRgbBackground(); + } + else + { + int key = (int) (inputValue * factor); + value = colorTable[key]; + } + int index = j * w + i; + int r = value & 255; + value >>= 8; + int g = value & 255; + value >>= 8; + int b = value & 255; + data[index] = Color.argb(255, r, g, b); + } + } + raster.setPixels(data, 0, w, 0, 0, w, h); + return raster; + } +} diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/AxialShadingPaint.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/AxialShadingPaint.java new file mode 100644 index 00000000..20867b65 --- /dev/null +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/AxialShadingPaint.java @@ -0,0 +1,35 @@ +package com.tom_roush.pdfbox.pdmodel.graphics.shading; + +import android.graphics.Rect; +import android.util.Log; + +import com.tom_roush.harmony.awt.geom.AffineTransform; +import com.tom_roush.pdfbox.rendering.PaintContext; +import com.tom_roush.pdfbox.util.Matrix; + +import java.io.IOException; + +public class AxialShadingPaint extends ShadingPaint{ + + /** + * Constructor. + * + * @param shadingType2 the shading resources + * @param matrix the pattern matrix concatenated with that of the parent content stream + */ + AxialShadingPaint(PDShadingType2 shadingType2, Matrix matrix) + { + super(shadingType2, matrix); + } + + @Override + public PaintContext createContext(Rect deviceBounds, AffineTransform xform) { + try { + return new AxialShadingContext(shading, xform, matrix, deviceBounds); + } + catch (IOException e){ + Log.e("Pdfbox-Android", "IOError while create paint context"); + return null; + } + } +} diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/CoonsPatch.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/CoonsPatch.java new file mode 100644 index 00000000..c1a8e0f4 --- /dev/null +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/CoonsPatch.java @@ -0,0 +1,197 @@ +package com.tom_roush.pdfbox.pdmodel.graphics.shading; + +import android.graphics.PointF; + +import java.util.List; + +public class CoonsPatch extends Patch{ + + /** + * Constructor of a patch for type 6 shading. + * + * @param points 12 control points + * @param color 4 corner colors + */ + protected CoonsPatch(PointF[] points, float[][] color) + { + super(color); + controlPoints = reshapeControlPoints(points); + level = calcLevel(); + listOfTriangles = getTriangles(); + } + + // adjust the 12 control points to 4 groups, each group defines one edge of a patch + private PointF[][] reshapeControlPoints(PointF[] points) + { + PointF[][] fourRows = new PointF[4][4]; + fourRows[2] = new PointF[] + { + points[0], points[1], points[2], points[3] + }; // d1 + fourRows[1] = new PointF[] + { + points[3], points[4], points[5], points[6] + }; // c2 + fourRows[3] = new PointF[] + { + points[9], points[8], points[7], points[6] + }; // d2 + fourRows[0] = new PointF[] + { + points[0], points[11], points[10], points[9] + }; // c1 + return fourRows; + } + + // calculate the dividing level from control points + private int[] calcLevel() + { + int[] l = + { + 4, 4 + }; + // if two opposite edges are both lines, there is a possibility to reduce the dividing level + if (isEdgeALine(controlPoints[0]) && isEdgeALine(controlPoints[1])) + { + double lc1 = getLen(controlPoints[0][0], controlPoints[0][3]), + lc2 = getLen(controlPoints[1][0], controlPoints[1][3]); + // determine the dividing level by the lengths of edges + if (lc1 > 800 || lc2 > 800) + { + // keeps init value 4 + } + else if (lc1 > 400 || lc2 > 400) + { + l[0] = 3; + } + else if (lc1 > 200 || lc2 > 200) + { + l[0] = 2; + } + else + { + l[0] = 1; + } + } + + // the other two opposite edges + if (isEdgeALine(controlPoints[2]) && isEdgeALine(controlPoints[3])) + { + double ld1 = getLen(controlPoints[2][0], controlPoints[2][3]), + ld2 = getLen(controlPoints[3][0], controlPoints[3][3]); + if (ld1 > 800 || ld2 > 800) + { + // keeps init value 4 + } + else if (ld1 > 400 || ld2 > 400) + { + l[1] = 3; + } + else if (ld1 > 200 || ld2 > 200) + { + l[1] = 2; + } + else + { + l[1] = 1; + } + } + return l; + } + + // get a list of triangles which compose this coons patch + private List getTriangles() + { + // 4 edges are 4 cubic Bezier curves + CubicBezierCurve eC1 = new CubicBezierCurve(controlPoints[0], level[0]); + CubicBezierCurve eC2 = new CubicBezierCurve(controlPoints[1], level[0]); + CubicBezierCurve eD1 = new CubicBezierCurve(controlPoints[2], level[1]); + CubicBezierCurve eD2 = new CubicBezierCurve(controlPoints[3], level[1]); + CoordinateColorPair[][] patchCC = getPatchCoordinatesColor(eC1, eC2, eD1, eD2); + return getShadedTriangles(patchCC); + } + + @Override + protected PointF[] getFlag1Edge() + { + return controlPoints[1].clone(); + } + + @Override + protected PointF[] getFlag2Edge() + { + PointF[] implicitEdge = new PointF[4]; + implicitEdge[0] = controlPoints[3][3]; + implicitEdge[1] = controlPoints[3][2]; + implicitEdge[2] = controlPoints[3][1]; + implicitEdge[3] = controlPoints[3][0]; + return implicitEdge; + } + + @Override + protected PointF[] getFlag3Edge() + { + PointF[] implicitEdge = new PointF[4]; + implicitEdge[0] = controlPoints[0][3]; + implicitEdge[1] = controlPoints[0][2]; + implicitEdge[2] = controlPoints[0][1]; + implicitEdge[3] = controlPoints[0][0]; + return implicitEdge; + } + + /* + dividing a patch into a grid, return a matrix of the coordinate and color at the crossing points of the grid, + the rule to calculate the coordinate is defined in page 195 of PDF32000_2008.pdf, the rule to calculate the + corresponding color is bilinear interpolation + */ + private CoordinateColorPair[][] getPatchCoordinatesColor(CubicBezierCurve c1, CubicBezierCurve c2, CubicBezierCurve d1, CubicBezierCurve d2) + { + PointF[] curveC1 = c1.getCubicBezierCurve(); + PointF[] curveC2 = c2.getCubicBezierCurve(); + PointF[] curveD1 = d1.getCubicBezierCurve(); + PointF[] curveD2 = d2.getCubicBezierCurve(); + + int numberOfColorComponents = cornerColor[0].length; + int szV = curveD1.length; + int szU = curveC1.length; + + CoordinateColorPair[][] patchCC = new CoordinateColorPair[szV][szU]; + + double stepV = (double) 1 / (szV - 1); + double stepU = (double) 1 / (szU - 1); + double v = -stepV; + for (int i = 0; i < szV; i++) + { + // v and u are the assistant parameters + v += stepV; + double u = -stepU; + for (int j = 0; j < szU; j++) + { + u += stepU; + double scx = (1 - v) * curveC1[j].x + v * curveC2[j].x; + double scy = (1 - v) * curveC1[j].y + v * curveC2[j].y; + double sdx = (1 - u) * curveD1[i].x + u * curveD2[i].x; + double sdy = (1 - u) * curveD1[i].y + u * curveD2[i].y; + double sbx = (1 - v) * ((1 - u) * controlPoints[0][0].x + u * controlPoints[0][3].x) + + v * ((1 - u) * controlPoints[1][0].x + u * controlPoints[1][3].x); + double sby = (1 - v) * ((1 - u) * controlPoints[0][0].y + u * controlPoints[0][3].y) + + v * ((1 - u) * controlPoints[1][0].y + u * controlPoints[1][3].y); + + double sx = scx + sdx - sbx; + double sy = scy + sdy - sby; + // the above code in this for loop defines the patch surface (coordinates) + + PointF tmpC = new PointF((float) sx, (float) sy); + + float[] paramSC = new float[numberOfColorComponents]; + for (int ci = 0; ci < numberOfColorComponents; ci++) + { + paramSC[ci] = (float) ((1 - v) * ((1 - u) * cornerColor[0][ci] + u * cornerColor[3][ci]) + + v * ((1 - u) * cornerColor[1][ci] + u * cornerColor[2][ci])); // bilinear interpolation + } + patchCC[i][j] = new CoordinateColorPair(tmpC, paramSC); + } + } + return patchCC; + } +} diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/CoordinateColorPair.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/CoordinateColorPair.java new file mode 100644 index 00000000..a3116a84 --- /dev/null +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/CoordinateColorPair.java @@ -0,0 +1,21 @@ +package com.tom_roush.pdfbox.pdmodel.graphics.shading; + +import android.graphics.PointF; + +public class CoordinateColorPair { + + final PointF coordinate; + final float[] color; + + /** + * Constructor. + * + * @param p point + * @param c color + */ + CoordinateColorPair(PointF p, float[] c) + { + coordinate = p; + color = c.clone(); + } +} diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/CubicBezierCurve.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/CubicBezierCurve.java new file mode 100644 index 00000000..990883e5 --- /dev/null +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/CubicBezierCurve.java @@ -0,0 +1,85 @@ +package com.tom_roush.pdfbox.pdmodel.graphics.shading; + +import android.graphics.PointF; + +public class CubicBezierCurve { + private final PointF[] controlPoints; + private final int level; + private final PointF[] curve; + + /** + * Constructor of CubicBezierCurve + * + * @param ctrlPnts 4 control points [p0, p1, p2, p3] + * @param l dividing level, if l = 0, one cubic Bezier curve is divided + * into 2^0 = 1 segments, if l = n, one cubic Bezier curve is divided into + * 2^n segments + */ + CubicBezierCurve(PointF[] ctrlPnts, int l) + { + controlPoints = ctrlPnts.clone(); + level = l; + curve = getPoints(level); + } + + /** + * Get level parameter + * + * @return level + */ + int getLevel() + { + return level; + } + + // calculate sampled points on the cubic Bezier curve defined by the 4 given control points + private PointF[] getPoints(int l) + { + if (l < 0) + { + l = 0; + } + int sz = (1 << l) + 1; + PointF[] res = new PointF[sz]; + double step = (double) 1 / (sz - 1); + double t = -step; + for (int i = 0; i < sz; i++) { + t += step; + double tmpX = (1 - t) * (1 - t) * (1 - t) * controlPoints[0].x + + 3 * t * (1 - t) * (1 - t) * controlPoints[1].x + + 3 * t * t * (1 - t) * controlPoints[2].x + + t * t * t * controlPoints[3].x; + double tmpY = (1 - t) * (1 - t) * (1 - t) * controlPoints[0].y + + 3 * t * (1 - t) * (1 - t) * controlPoints[1].y + + 3 * t * t * (1 - t) * controlPoints[2].y + + t * t * t * controlPoints[3].y; + res[i] = new PointF((float) tmpX, (float) tmpY); + } + return res; + } + + /** + * Get sampled points of this cubic Bezier curve. + * + * @return sampled points + */ + PointF[] getCubicBezierCurve() + { + return curve; + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + for (PointF p : controlPoints) + { + if (sb.length() > 0) + { + sb.append(' '); + } + sb.append(p); + } + return "Cubic Bezier curve{control points p0, p1, p2, p3: " + sb + "}"; + } +} diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/GouraudShadingContext.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/GouraudShadingContext.java new file mode 100644 index 00000000..d979ea9c --- /dev/null +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/GouraudShadingContext.java @@ -0,0 +1,61 @@ +package com.tom_roush.pdfbox.pdmodel.graphics.shading; + +import android.graphics.Point; +import android.graphics.Rect; + +import com.tom_roush.harmony.awt.geom.AffineTransform; +import com.tom_roush.pdfbox.util.Matrix; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class GouraudShadingContext extends TriangleBasedShadingContext{ + + /** + * triangle list. + */ + private List triangleList = new ArrayList(); + + /** + * Constructor creates an instance to be used for fill operations. + * + * @param shading the shading type to be used + * @param xform transformation for user to device space + * @param matrix the pattern matrix concatenated with that of the parent content stream + * @throws IOException if something went wrong + */ + protected GouraudShadingContext(PDShading shading, AffineTransform xform, + Matrix matrix) throws IOException + { + super(shading, xform, matrix); + } + + final void setTriangleList(List triangleList) + { + this.triangleList = triangleList; + } + + @Override + protected Map calcPixelTable(Rect deviceBounds) throws IOException + { + Map map = new HashMap(); + super.calcPixelTable(triangleList, map, deviceBounds); + return map; + } + + @Override + public void dispose() + { + triangleList = null; + super.dispose(); + } + + @Override + protected boolean isDataEmpty() + { + return triangleList.isEmpty(); + } +} diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/Line.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/Line.java new file mode 100644 index 00000000..729f2718 --- /dev/null +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/Line.java @@ -0,0 +1,111 @@ +package com.tom_roush.pdfbox.pdmodel.graphics.shading; + +import android.graphics.Point; + +import java.util.HashSet; +import java.util.Set; + +public class Line { + + private final Point point0; + private final Point point1; + private final float[] color0; + private final float[] color1; + + protected final Set linePoints; // all the points in this rasterized line + + /** + * Constructor of class Line. + * + * @param p0 one end of a line + * @param p1 the other end of the line + * @param c0 color of point p0 + * @param c1 color of point p1 + */ + Line(Point p0, Point p1, float[] c0, float[] c1) + { + point0 = p0; + point1 = p1; + color0 = c0.clone(); + color1 = c1.clone(); + linePoints = calcLine(point0.x, point0.y, point1.x, point1.y); + } + + /** + * Calculate the points of a line with Bresenham's line algorithm + * Bresenham's + * line algorithm + * + * @param x0 coordinate + * @param y0 coordinate + * @param x1 coordinate + * @param y1 coordinate + * @return all the points on the rasterized line from (x0, y0) to (x1, y1) + */ + private Set calcLine(int x0, int y0, int x1, int y1) + { + Set points = new HashSet(3); + int dx = Math.abs(x1 - x0); + int dy = Math.abs(y1 - y0); + int sx = x0 < x1 ? 1 : -1; + int sy = y0 < y1 ? 1 : -1; + int err = dx - dy; + while (true) + { + points.add(new Point(x0, y0)); + if (x0 == x1 && y0 == y1) + { + break; + } + int e2 = 2 * err; + if (e2 > -dy) + { + err -= dy; + x0 += sx; + } + if (e2 < dx) + { + err += dx; + y0 += sy; + } + } + return points; + } + + /** + * Calculate the color of a point on a rasterized line by linear + * interpolation. + * + * @param p target point, p should always be contained in linePoints + * @return color + */ + protected float[] calcColor(Point p) + { + if (point0.x == point1.x && point0.y == point1.y) + { + return color0; + } + int numberOfColorComponents = color0.length; + float[] pc = new float[numberOfColorComponents]; + if (point0.x == point1.x) + { + float l = point1.y - point0.y; + for (int i = 0; i < numberOfColorComponents; i++) + { + pc[i] = (color0[i] * (point1.y - p.y) / l + + color1[i] * (p.y - point0.y) / l); + } + } + else + { + float l = point1.x - point0.x; + for (int i = 0; i < numberOfColorComponents; i++) + { + pc[i] = (color0[i] * (point1.x - p.x) / l + + color1[i] * (p.x - point0.x) / l); + } + } + return pc; + } +} diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/PDMeshBasedShadingType.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/PDMeshBasedShadingType.java new file mode 100644 index 00000000..0dcf6415 --- /dev/null +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/PDMeshBasedShadingType.java @@ -0,0 +1,246 @@ +package com.tom_roush.pdfbox.pdmodel.graphics.shading; + +import android.graphics.PointF; +import android.graphics.RectF; +import android.util.Log; + +import com.tom_roush.harmony.awt.geom.AffineTransform; +import com.tom_roush.harmony.javax.imageio.stream.ImageInputStream; +import com.tom_roush.harmony.javax.imageio.stream.MemoryCacheImageInputStream; +import com.tom_roush.pdfbox.cos.COSDictionary; +import com.tom_roush.pdfbox.cos.COSStream; +import com.tom_roush.pdfbox.pdmodel.common.PDRange; +import com.tom_roush.pdfbox.util.GraphicsUtil; +import com.tom_roush.pdfbox.util.Matrix; + +import java.io.EOFException; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +abstract class PDMeshBasedShadingType extends PDShadingType4{ + + PDMeshBasedShadingType(COSDictionary shadingDictionary) + { + super(shadingDictionary); + } + + /** + * Create a patch list from a data stream, the returned list contains all the patches contained in the data stream. + * + * @param xform transformation for user to device space + * @param matrix the pattern matrix concatenated with that of the parent content stream + * @param controlPoints number of control points, 12 for type 6 shading and 16 for type 7 shading + * @return the obtained patch list + * @throws IOException when something went wrong + */ + @SuppressWarnings({ "squid:S2583", "squid:S1166" }) + final List collectPatches(AffineTransform xform, Matrix matrix, int controlPoints) + throws IOException + { + COSDictionary dict = getCOSObject(); + if (!(dict instanceof COSStream)) + { + return Collections.emptyList(); + } + PDRange rangeX = getDecodeForParameter(0); + PDRange rangeY = getDecodeForParameter(1); + if (rangeX == null || rangeY == null || + Float.compare(rangeX.getMin(), rangeX.getMax()) == 0 || + Float.compare(rangeY.getMin(), rangeY.getMax()) == 0) + { + return Collections.emptyList(); + } + int bitsPerFlag = getBitsPerFlag(); + PDRange[] colRange = new PDRange[getNumberOfColorComponents()]; + for (int i = 0; i < colRange.length; ++i) + { + colRange[i] = getDecodeForParameter(2 + i); + if (colRange[i] == null) + { + throw new IOException("Range missing in shading /Decode entry"); + } + } + List list = new ArrayList(); + long maxSrcCoord = (long) Math.pow(2, getBitsPerCoordinate()) - 1; + long maxSrcColor = (long) Math.pow(2, getBitsPerComponent()) - 1; + COSStream cosStream = (COSStream) dict; + + ImageInputStream mciis = new MemoryCacheImageInputStream(cosStream.createInputStream()); + try + { + PointF[] implicitEdge = new PointF[4]; + float[][] implicitCornerColor = new float[2][colRange.length]; + byte flag = 0; + + try + { + flag = (byte) (mciis.readBits(bitsPerFlag) & 3); + } + catch (EOFException ex) + { + Log.e("Pdfbox-Android", ex.getMessage()); + return list; + } + + boolean eof = false; + while (!eof) + { + try + { + boolean isFree = (flag == 0); + Patch current = readPatch(mciis, isFree, implicitEdge, implicitCornerColor, + maxSrcCoord, maxSrcColor, rangeX, rangeY, colRange, matrix, xform, + controlPoints); + if (current == null) + { + break; + } + list.add(current); + flag = (byte) (mciis.readBits(bitsPerFlag) & 3); + switch (flag) + { + case 0: + break; + case 1: + implicitEdge = current.getFlag1Edge(); + implicitCornerColor = current.getFlag1Color(); + break; + case 2: + implicitEdge = current.getFlag2Edge(); + implicitCornerColor = current.getFlag2Color(); + break; + case 3: + implicitEdge = current.getFlag3Edge(); + implicitCornerColor = current.getFlag3Color(); + break; + default: + Log.w("Pdfbox-Android", "bad flag: " + flag); + break; + } + } + catch (EOFException ex) + { + eof = true; + } + } + } + finally + { + mciis.close(); + } + return list; + } + + /** + * Read a single patch from a data stream, a patch contains information of its coordinates and color parameters. + * + * @param input the image source data stream + * @param isFree whether this is a free patch + * @param implicitEdge implicit edge when a patch is not free, otherwise it's not used + * @param implicitCornerColor implicit colors when a patch is not free, otherwise it's not used + * @param maxSrcCoord the maximum coordinate value calculated from source data + * @param maxSrcColor the maximum color value calculated from source data + * @param rangeX range for coordinate x + * @param rangeY range for coordinate y + * @param colRange range for color + * @param matrix the pattern matrix concatenated with that of the parent content stream + * @param xform transformation for user to device space + * @param controlPoints number of control points, 12 for type 6 shading and 16 for type 7 shading + * @return a single patch + * @throws IOException when something went wrong + */ + protected Patch readPatch(ImageInputStream input, boolean isFree, PointF[] implicitEdge, + float[][] implicitCornerColor, long maxSrcCoord, long maxSrcColor, PDRange rangeX, + PDRange rangeY, PDRange[] colRange, Matrix matrix, AffineTransform xform, + int controlPoints) throws IOException + { + int numberOfColorComponents = getNumberOfColorComponents(); + float[][] color = new float[4][numberOfColorComponents]; + PointF[] points = new PointF[controlPoints]; + int pStart = 4; + int cStart = 2; + if (isFree) + { + pStart = 0; + cStart = 0; + } + else + { + points[0] = implicitEdge[0]; + points[1] = implicitEdge[1]; + points[2] = implicitEdge[2]; + points[3] = implicitEdge[3]; + + for (int i = 0; i < numberOfColorComponents; i++) + { + color[0][i] = implicitCornerColor[0][i]; + color[1][i] = implicitCornerColor[1][i]; + } + } + + try + { + for (int i = pStart; i < controlPoints; i++) + { + long x = input.readBits(getBitsPerCoordinate()); + long y = input.readBits(getBitsPerCoordinate()); + float px = interpolate(x, maxSrcCoord, rangeX.getMin(), rangeX.getMax()); + float py = interpolate(y, maxSrcCoord, rangeY.getMin(), rangeY.getMax()); + PointF p = matrix.transformPoint(px, py); + xform.transform(p, p); + points[i] = p; + } + for (int i = cStart; i < 4; i++) + { + for (int j = 0; j < numberOfColorComponents; j++) + { + long c = input.readBits(getBitsPerComponent()); + color[i][j] = interpolate(c, maxSrcColor, colRange[j].getMin(), + colRange[j].getMax()); + } + } + } + catch (EOFException ex) + { + Log.d("Pdfbox-Android", "EOF", ex); + return null; + } + return generatePatch(points, color); + } + + /** + * Create a patch using control points and 4 corner color values, in Type6ShadingContext, a CoonsPatch is returned; + * in Type6ShadingContext, a TensorPatch is returned. + * + * @param points 12 or 16 control points + * @param color 4 corner colors + * @return a patch instance + */ + abstract Patch generatePatch(PointF[] points, float[][] color); + + @Override + public abstract RectF getBounds(AffineTransform xform, Matrix matrix) throws IOException; + + RectF getBounds(AffineTransform xform, Matrix matrix, int controlPoints) + throws IOException + { + RectF bounds = null; + for (Patch patch : collectPatches(xform, matrix, controlPoints)) + { + for (ShadedTriangle shadedTriangle : patch.listOfTriangles) + { + if (bounds == null) + { + bounds = new RectF(shadedTriangle.corner[0].x, + shadedTriangle.corner[0].y, 0, 0); + } + GraphicsUtil.addPoint2Rect(bounds, shadedTriangle.corner[0]); + GraphicsUtil.addPoint2Rect(bounds, shadedTriangle.corner[1]); + GraphicsUtil.addPoint2Rect(bounds, shadedTriangle.corner[2]); + } + } + return bounds; + } +} diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/PDShading.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/PDShading.java index 294195eb..cb322ade 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/PDShading.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/PDShading.java @@ -16,8 +16,11 @@ */ package com.tom_roush.pdfbox.pdmodel.graphics.shading; +import android.graphics.RectF; + import java.io.IOException; +import com.tom_roush.harmony.awt.geom.AffineTransform; import com.tom_roush.pdfbox.cos.COSArray; import com.tom_roush.pdfbox.cos.COSBase; import com.tom_roush.pdfbox.cos.COSDictionary; @@ -26,6 +29,8 @@ import com.tom_roush.pdfbox.pdmodel.common.PDRectangle; import com.tom_roush.pdfbox.pdmodel.common.function.PDFunction; import com.tom_roush.pdfbox.pdmodel.graphics.color.PDColorSpace; +import com.tom_roush.pdfbox.rendering.WrapPaint; +import com.tom_roush.pdfbox.util.Matrix; /** * A Shading Resource. @@ -194,6 +199,19 @@ public void setBBox(PDRectangle newBBox) } } + /** + * Calculate a bounding rectangle around the areas of this shading context. + * + * @param xform + * @param matrix + * @return Bounding rectangle or null, if not supported by this shading type. + * @throws java.io.IOException + */ + public RectF getBounds(AffineTransform xform, Matrix matrix) throws IOException + { + return null; + } + /** * This will set the AntiAlias value. * @@ -426,5 +444,5 @@ else if (returnValues[i] > 1) * this matrix which maps the pattern's internal coordinate system to user space * @return an AWT Paint instance */ -// public abstract Paint toPaint(Matrix matrix); TODO: PdfBox-Android + public abstract WrapPaint toPaint(Matrix matrix); } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/PDShadingType1.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/PDShadingType1.java index 7b9b3a94..4a731b18 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/PDShadingType1.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/PDShadingType1.java @@ -21,6 +21,7 @@ import com.tom_roush.pdfbox.cos.COSDictionary; import com.tom_roush.pdfbox.cos.COSFloat; import com.tom_roush.pdfbox.cos.COSName; +import com.tom_roush.pdfbox.rendering.WrapPaint; import com.tom_roush.pdfbox.util.Matrix; /** @@ -98,5 +99,8 @@ public void setDomain(COSArray newDomain) getCOSObject().setItem(COSName.DOMAIN, newDomain); } -// public Paint toPaint(Matrix matrix) TODO: PdfBox-Android + @Override + public WrapPaint toPaint(Matrix matrix){ + return new Type1ShadingPaint(this, matrix); + } } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/PDShadingType2.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/PDShadingType2.java index d041a0fe..498be224 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/PDShadingType2.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/PDShadingType2.java @@ -19,6 +19,8 @@ import com.tom_roush.pdfbox.cos.COSArray; import com.tom_roush.pdfbox.cos.COSDictionary; import com.tom_roush.pdfbox.cos.COSName; +import com.tom_roush.pdfbox.rendering.WrapPaint; +import com.tom_roush.pdfbox.util.Matrix; /** * Resources for an axial shading. @@ -120,9 +122,9 @@ public void setCoords(COSArray newCoords) getCOSObject().setItem(COSName.COORDS, newCoords); } -// @Override -// public Paint toPaint(Matrix matrix) -// { -// return new AxialShadingPaint(this, matrix); -// }TODO: PdfBox-Android + @Override + public WrapPaint toPaint(Matrix matrix) + { + return new AxialShadingPaint(this, matrix); + } } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/PDShadingType3.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/PDShadingType3.java index 7b550dbd..241dc707 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/PDShadingType3.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/PDShadingType3.java @@ -17,6 +17,8 @@ package com.tom_roush.pdfbox.pdmodel.graphics.shading; import com.tom_roush.pdfbox.cos.COSDictionary; +import com.tom_roush.pdfbox.rendering.WrapPaint; +import com.tom_roush.pdfbox.util.Matrix; /** * Resources for a radial shading. @@ -39,9 +41,9 @@ public int getShadingType() return PDShading.SHADING_TYPE3; } -// @Override -// public Paint toPaint(Matrix matrix) -// { -// return new RadialShadingPaint(this, matrix); -// }TODO: PdfBox-Android + @Override + public WrapPaint toPaint(Matrix matrix) + { + return new RadialShadingPaint(this, matrix); + } } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/PDShadingType4.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/PDShadingType4.java index fd325c23..579bf8b1 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/PDShadingType4.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/PDShadingType4.java @@ -16,8 +16,24 @@ */ package com.tom_roush.pdfbox.pdmodel.graphics.shading; +import android.graphics.PointF; +import android.util.Log; + +import com.tom_roush.harmony.awt.geom.AffineTransform; +import com.tom_roush.harmony.javax.imageio.stream.ImageInputStream; +import com.tom_roush.harmony.javax.imageio.stream.MemoryCacheImageInputStream; import com.tom_roush.pdfbox.cos.COSDictionary; import com.tom_roush.pdfbox.cos.COSName; +import com.tom_roush.pdfbox.cos.COSStream; +import com.tom_roush.pdfbox.pdmodel.common.PDRange; +import com.tom_roush.pdfbox.rendering.WrapPaint; +import com.tom_roush.pdfbox.util.Matrix; + +import java.io.EOFException; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; /** * Resources for a shading type 4 (Free-Form Gouraud-Shaded Triangle Mesh). @@ -61,9 +77,145 @@ public void setBitsPerFlag(int bitsPerFlag) getCOSObject().setInt(COSName.BITS_PER_FLAG, bitsPerFlag); } -// @Override -// public Paint toPaint(Matrix matrix) -// { -// return new Type4ShadingPaint(this, matrix); -// }TODO: PdfBox-Android + @Override + public WrapPaint toPaint(Matrix matrix) + { + return new Type4ShadingPaint(this, matrix); + } + + @Override + List collectTriangles(AffineTransform xform, Matrix matrix) + throws IOException + { + int bitsPerFlag = getBitsPerFlag(); + COSDictionary dict = getCOSObject(); + if (!(dict instanceof COSStream)) + { + return Collections.emptyList(); + } + PDRange rangeX = getDecodeForParameter(0); + PDRange rangeY = getDecodeForParameter(1); + if (rangeX == null || rangeY == null || + Float.compare(rangeX.getMin(), rangeX.getMax()) == 0 || + Float.compare(rangeY.getMin(), rangeY.getMax()) == 0) + { + return Collections.emptyList(); + } + PDRange[] colRange = new PDRange[getNumberOfColorComponents()]; + for (int i = 0; i < colRange.length; ++i) + { + colRange[i] = getDecodeForParameter(2 + i); + if (colRange[i] == null) + { + throw new IOException("Range missing in shading /Decode entry"); + } + } + List list = new ArrayList(); + long maxSrcCoord = (long) Math.pow(2, getBitsPerCoordinate()) - 1; + long maxSrcColor = (long) Math.pow(2, getBitsPerComponent()) - 1; + COSStream stream = (COSStream) dict; + + ImageInputStream mciis = new MemoryCacheImageInputStream(stream.createInputStream()); + try + { + byte flag = (byte) 0; + try + { + flag = (byte) (mciis.readBits(bitsPerFlag) & 3); + } + catch (EOFException ex) + { + Log.e("Pdfbox-Android", ex.getMessage()); + } + + boolean eof = false; + while (!eof) + { + Vertex p0; + Vertex p1; + Vertex p2; + PointF[] ps; + float[][] cs; + int lastIndex; + try + { + switch (flag) + { + case 0: + p0 = readVertex(mciis, maxSrcCoord, maxSrcColor, rangeX, rangeY, colRange, + matrix, xform); + flag = (byte) (mciis.readBits(bitsPerFlag) & 3); + if (flag != 0) + { + Log.e("Pdfbox-Android", "bad triangle: " + flag); + } + p1 = readVertex(mciis, maxSrcCoord, maxSrcColor, rangeX, rangeY, colRange, + matrix, xform); + mciis.readBits(bitsPerFlag); + if (flag != 0) + { + Log.e("Pdfbox-Android", "bad triangle: " + flag); + } + p2 = readVertex(mciis, maxSrcCoord, maxSrcColor, rangeX, rangeY, colRange, + matrix, xform); + if(isAllVertexsZero(p0, p1, p2)){ + throw new EOFException("end"); + } + ps = new PointF[] { p0.point, p1.point, p2.point }; + cs = new float[][] { p0.color, p1.color, p2.color }; + list.add(new ShadedTriangle(ps, cs)); + // here should raise EOFException in pdfbox, but it not raised in android, so we check isAllVertexsZero and raise it manually + flag = (byte) (mciis.readBits(bitsPerFlag) & 3); + break; + case 1: + case 2: + lastIndex = list.size() - 1; + if (lastIndex < 0) + { + Log.e("Pdfbox-Android", "broken data stream: " + list.size()); + } + else + { + ShadedTriangle preTri = list.get(lastIndex); + p2 = readVertex(mciis, maxSrcCoord, maxSrcColor, rangeX, rangeY, + colRange, matrix, xform); + ps = new PointF[] { flag == 1 ? preTri.corner[1] : preTri.corner[0], + preTri.corner[2], + p2.point }; + cs = new float[][] { flag == 1 ? preTri.color[1] : preTri.color[0], + preTri.color[2], + p2.color }; + list.add(new ShadedTriangle(ps, cs)); + flag = (byte) (mciis.readBits(bitsPerFlag) & 3); + } + break; + default: + Log.w("Pdfbox-Android", "bad flag: " + flag); + break; + } + } + catch (EOFException ex) + { + eof = true; + } + } + } + finally + { + mciis.close(); + } + return list; + } + + /** + * check whether the read vertex is all zero + * @param p0 + * @param p1 + * @param p2 + * @return + */ + private boolean isAllVertexsZero(Vertex p0, Vertex p1, Vertex p2) { + return p0.point.x + p0.point.y == 0 && p1.point.x + p1.point.y == 0 && p2.point.x + p2.point.y == 0; + } + } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/PDShadingType5.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/PDShadingType5.java index 586452fd..36526729 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/PDShadingType5.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/PDShadingType5.java @@ -16,8 +16,23 @@ */ package com.tom_roush.pdfbox.pdmodel.graphics.shading; +import android.graphics.PointF; + +import com.tom_roush.harmony.awt.geom.AffineTransform; +import com.tom_roush.harmony.javax.imageio.stream.ImageInputStream; +import com.tom_roush.harmony.javax.imageio.stream.MemoryCacheImageInputStream; import com.tom_roush.pdfbox.cos.COSDictionary; import com.tom_roush.pdfbox.cos.COSName; +import com.tom_roush.pdfbox.cos.COSStream; +import com.tom_roush.pdfbox.pdmodel.common.PDRange; +import com.tom_roush.pdfbox.rendering.WrapPaint; +import com.tom_roush.pdfbox.util.Matrix; + +import java.io.EOFException; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; /** * Resources for a shading type 5 (Lattice-Form Gouraud-Shade Triangle Mesh). @@ -61,9 +76,112 @@ public void setVerticesPerRow(int verticesPerRow) getCOSObject().setInt(COSName.VERTICES_PER_ROW, verticesPerRow); } -// @Override -// public Paint toPaint(Matrix matrix) -// { -// return new Type5ShadingPaint(this, matrix); -// }TODO: PdfBox-Android + @Override + public WrapPaint toPaint(Matrix matrix) + { + return new Type5ShadingPaint(this, matrix); + } + @Override + List collectTriangles(AffineTransform xform, Matrix matrix) throws IOException + { + COSDictionary dict = getCOSObject(); + if (!(dict instanceof COSStream)) + { + return Collections.emptyList(); + } + PDRange rangeX = getDecodeForParameter(0); + PDRange rangeY = getDecodeForParameter(1); + if (rangeX == null || rangeY == null || + Float.compare(rangeX.getMin(), rangeX.getMax()) == 0 || + Float.compare(rangeY.getMin(), rangeY.getMax()) == 0) + { + return Collections.emptyList(); + } + int numPerRow = getVerticesPerRow(); + PDRange[] colRange = new PDRange[getNumberOfColorComponents()]; + for (int i = 0; i < colRange.length; ++i) + { + colRange[i] = getDecodeForParameter(2 + i); + if (colRange[i] == null) + { + throw new IOException("Range missing in shading /Decode entry"); + } + } + List vlist = new ArrayList(); + long maxSrcCoord = (long) Math.pow(2, getBitsPerCoordinate()) - 1; + long maxSrcColor = (long) Math.pow(2, getBitsPerComponent()) - 1; + COSStream cosStream = (COSStream) dict; + + ImageInputStream mciis = new MemoryCacheImageInputStream(cosStream.createInputStream()); + try + { + boolean eof = false; + while (!eof) + { + Vertex p; + try + { + p = readVertex(mciis, maxSrcCoord, maxSrcColor, rangeX, rangeY, colRange, matrix, xform); + vlist.add(p); + } + catch (EOFException ex) + { + eof = true; + } + } + } + finally + { + mciis.close(); + } + int rowNum = vlist.size() / numPerRow; + if (rowNum < 2) + { + // must have at least two rows; if not, return empty list + return Collections.emptyList(); + } + Vertex[][] latticeArray = new Vertex[rowNum][numPerRow]; + for (int i = 0; i < rowNum; i++) + { + for (int j = 0; j < numPerRow; j++) + { + latticeArray[i][j] = vlist.get(i * numPerRow + j); + } + } + + return createShadedTriangleList(rowNum, numPerRow, latticeArray); + } + + private List createShadedTriangleList(int rowNum, int numPerRow, Vertex[][] latticeArray) + { + PointF[] ps = new PointF[3]; // array will be shallow-cloned in ShadedTriangle constructor + float[][] cs = new float[3][]; + List list = new ArrayList(); + for (int i = 0; i < rowNum - 1; i++) + { + for (int j = 0; j < numPerRow - 1; j++) + { + ps[0] = latticeArray[i][j].point; + ps[1] = latticeArray[i][j + 1].point; + ps[2] = latticeArray[i + 1][j].point; + + cs[0] = latticeArray[i][j].color; + cs[1] = latticeArray[i][j + 1].color; + cs[2] = latticeArray[i + 1][j].color; + + list.add(new ShadedTriangle(ps, cs)); + + ps[0] = latticeArray[i][j + 1].point; + ps[1] = latticeArray[i + 1][j].point; + ps[2] = latticeArray[i + 1][j + 1].point; + + cs[0] = latticeArray[i][j + 1].color; + cs[1] = latticeArray[i + 1][j].color; + cs[2] = latticeArray[i + 1][j + 1].color; + + list.add(new ShadedTriangle(ps, cs)); + } + } + return list; + } } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/PDShadingType6.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/PDShadingType6.java index 8c961eed..1ffcc0f5 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/PDShadingType6.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/PDShadingType6.java @@ -16,12 +16,20 @@ */ package com.tom_roush.pdfbox.pdmodel.graphics.shading; +import android.graphics.PointF; +import android.graphics.RectF; + +import com.tom_roush.harmony.awt.geom.AffineTransform; import com.tom_roush.pdfbox.cos.COSDictionary; +import com.tom_roush.pdfbox.rendering.WrapPaint; +import com.tom_roush.pdfbox.util.Matrix; + +import java.io.IOException; /** * Resources for a shading type 6 (Coons Patch Mesh). */ -public class PDShadingType6 extends PDShadingType4 +public class PDShadingType6 extends PDMeshBasedShadingType { /** * Constructor using the given shading dictionary. @@ -39,9 +47,21 @@ public int getShadingType() return PDShading.SHADING_TYPE6; } -// @Override -// public Paint toPaint(Matrix matrix) -// { -// return new Type6ShadingPaint(this, matrix); -// }TODO: PdfBox-Android + @Override + public WrapPaint toPaint(Matrix matrix) + { + return new Type6ShadingPaint(this, matrix); + } + + @Override + protected Patch generatePatch(PointF[] points, float[][] color) + { + return new CoonsPatch(points, color); + } + + @Override + public RectF getBounds(AffineTransform xform, Matrix matrix) throws IOException + { + return getBounds(xform, matrix, 12); + } } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/PDShadingType7.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/PDShadingType7.java index 18d5b85b..b9f5c170 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/PDShadingType7.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/PDShadingType7.java @@ -16,7 +16,15 @@ */ package com.tom_roush.pdfbox.pdmodel.graphics.shading; +import android.graphics.PointF; +import android.graphics.RectF; + +import com.tom_roush.harmony.awt.geom.AffineTransform; import com.tom_roush.pdfbox.cos.COSDictionary; +import com.tom_roush.pdfbox.rendering.WrapPaint; +import com.tom_roush.pdfbox.util.Matrix; + +import java.io.IOException; /** * Resources for a shading type 7 (Tensor-Product Patch Mesh). @@ -39,9 +47,21 @@ public int getShadingType() return PDShading.SHADING_TYPE7; } -// @Override -// public Paint toPaint(Matrix matrix) -// { -// return new Type7ShadingPaint(this, matrix); -// }TODO: PdfBox-Android + @Override + public WrapPaint toPaint(Matrix matrix) + { + return new Type7ShadingPaint(this, matrix); + } + + @Override + protected Patch generatePatch(PointF[] points, float[][] color) + { + return new TensorPatch(points, color); + } + + @Override + public RectF getBounds(AffineTransform xform, Matrix matrix) throws IOException + { + return getBounds(xform, matrix, 16); + } } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/PDTriangleBasedShadingType.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/PDTriangleBasedShadingType.java index 0575466a..e9692cb4 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/PDTriangleBasedShadingType.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/PDTriangleBasedShadingType.java @@ -15,10 +15,21 @@ */ package com.tom_roush.pdfbox.pdmodel.graphics.shading; +import android.graphics.PointF; +import android.graphics.RectF; +import android.util.Log; + +import com.tom_roush.harmony.awt.geom.AffineTransform; +import com.tom_roush.harmony.javax.imageio.stream.ImageInputStream; import com.tom_roush.pdfbox.cos.COSArray; import com.tom_roush.pdfbox.cos.COSDictionary; import com.tom_roush.pdfbox.cos.COSName; import com.tom_roush.pdfbox.pdmodel.common.PDRange; +import com.tom_roush.pdfbox.util.GraphicsUtil; +import com.tom_roush.pdfbox.util.Matrix; + +import java.io.IOException; +import java.util.List; /** * Common resources for shading types 4,5,6 and 7 @@ -30,6 +41,10 @@ abstract class PDTriangleBasedShadingType extends PDShading // value: same as the value of Range private COSArray decode = null; + private int bitsPerCoordinate = -1; + private int bitsPerColorComponent = -1; + private int numberOfColorComponents = -1; + PDTriangleBasedShadingType(COSDictionary shadingDictionary) { super(shadingDictionary); @@ -43,7 +58,10 @@ abstract class PDTriangleBasedShadingType extends PDShading */ public int getBitsPerComponent() { - return getCOSObject().getInt(COSName.BITS_PER_COMPONENT, -1); + if(bitsPerColorComponent == -1){ + bitsPerColorComponent = getCOSObject().getInt(COSName.BITS_PER_COMPONENT, -1); + } + return bitsPerColorComponent; } /** @@ -54,6 +72,7 @@ public int getBitsPerComponent() public void setBitsPerComponent(int bitsPerComponent) { getCOSObject().setInt(COSName.BITS_PER_COMPONENT, bitsPerComponent); + bitsPerColorComponent = bitsPerComponent; } /** @@ -64,17 +83,37 @@ public void setBitsPerComponent(int bitsPerComponent) */ public int getBitsPerCoordinate() { - return getCOSObject().getInt(COSName.BITS_PER_COORDINATE, -1); + if(bitsPerCoordinate == -1){ + bitsPerCoordinate = getCOSObject().getInt(COSName.BITS_PER_COORDINATE, -1); + } + return bitsPerCoordinate; } /** * Set the number of bits per coordinate. * - * @param bitsPerComponent the number of bits per coordinate + * @param bitsPerCoordinate the number of bits per coordinate + */ + public void setBitsPerCoordinate(int bitsPerCoordinate) + { + getCOSObject().setInt(COSName.BITS_PER_COORDINATE, bitsPerCoordinate); + this.bitsPerCoordinate = bitsPerCoordinate; + } + + /** + * The number of color components of this shading. + * + * @return number of color components of this shading */ - public void setBitsPerCoordinate(int bitsPerComponent) + public int getNumberOfColorComponents() throws IOException { - getCOSObject().setInt(COSName.BITS_PER_COORDINATE, bitsPerComponent); + if (numberOfColorComponents == -1) + { + numberOfColorComponents = getFunction() != null ? 1 + : getColorSpace().getNumberOfComponents(); + Log.e("Pdfbox-Android", "numberOfColorComponents: " + numberOfColorComponents); + } + return numberOfColorComponents; } /** @@ -119,4 +158,89 @@ public PDRange getDecodeForParameter(int paramNum) return retval; } + /** + * Calculate the interpolation, see p.345 pdf spec 1.7. + * + * @param src src value + * @param srcMax max src value (2^bits-1) + * @param dstMin min dst value + * @param dstMax max dst value + * @return interpolated value + */ + protected float interpolate(float src, long srcMax, float dstMin, float dstMax) + { + return dstMin + (src * (dstMax - dstMin) / srcMax); + } + + /** + * Read a vertex from the bit input stream performs interpolations. + * + * @param input bit input stream + * @param maxSrcCoord max value for source coordinate (2^bits-1) + * @param maxSrcColor max value for source color (2^bits-1) + * @param rangeX dest range for X + * @param rangeY dest range for Y + * @param colRangeTab dest range array for colors + * @param matrix the pattern matrix concatenated with that of the parent content stream + * @return a new vertex with the flag and the interpolated values + * @throws IOException if something went wrong + */ + protected Vertex readVertex(ImageInputStream input, long maxSrcCoord, long maxSrcColor, + PDRange rangeX, PDRange rangeY, PDRange[] colRangeTab, + Matrix matrix, AffineTransform xform) throws IOException + { + float[] colorComponentTab = new float[numberOfColorComponents]; + long x = input.readBits(bitsPerCoordinate); + long y = input.readBits(bitsPerCoordinate); + float dstX = interpolate(x, maxSrcCoord, rangeX.getMin(), rangeX.getMax()); + float dstY = interpolate(y, maxSrcCoord, rangeY.getMin(), rangeY.getMax()); + // Log.d("Pdfbox-Android", "coord: " + String.format("[%06X,%06X] -> [%f,%f], num: %d", x, y, dstX, dstY, numberOfColorComponents)); + PointF p = matrix.transformPoint(dstX, dstY); + xform.transform(p, p); + + for (int n = 0; n < numberOfColorComponents; ++n) + { + int color = (int) input.readBits(bitsPerColorComponent); + colorComponentTab[n] = interpolate(color, maxSrcColor, colRangeTab[n].getMin(), + colRangeTab[n].getMax()); +// Log.d("Pdfbox-Android", "color[" + n + "]: " + color + "/" + String.format("%02x", color) +// + "-> color[" + n + "]: " + colorComponentTab[n]); + } + + // "Each set of vertex data shall occupy a whole number of bytes. + // If the total number of bits required is not divisible by 8, the last data byte + // for each vertex is padded at the end with extra bits, which shall be ignored." + int bitOffset = input.getBitOffset(); + if (bitOffset != 0) + { + input.readBits(8 - bitOffset); + } + + return new Vertex(p, colorComponentTab); + } + + abstract List collectTriangles(AffineTransform xform, Matrix matrix) throws IOException; + + @Override + public RectF getBounds(AffineTransform xform, Matrix matrix) throws IOException + { + RectF bounds = null; + for (ShadedTriangle shadedTriangle : collectTriangles(xform, matrix)) + { + if (bounds == null) + { + bounds = new RectF(shadedTriangle.corner[0].x, + shadedTriangle.corner[0].y, 0, 0); + } + GraphicsUtil.addPoint2Rect(bounds, shadedTriangle.corner[0]); + GraphicsUtil.addPoint2Rect(bounds, shadedTriangle.corner[1]); + GraphicsUtil.addPoint2Rect(bounds, shadedTriangle.corner[2]); + } + if (bounds == null) + { + // Speeds up files where triangles are empty, e.g. ghostscript file 690425 + return new RectF(); + } + return bounds; + } } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/Patch.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/Patch.java new file mode 100644 index 00000000..2aab25fc --- /dev/null +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/Patch.java @@ -0,0 +1,212 @@ +package com.tom_roush.pdfbox.pdmodel.graphics.shading; + +import android.graphics.PointF; + +import java.util.ArrayList; +import java.util.List; + +abstract class Patch { + + protected PointF[][] controlPoints; + protected float[][] cornerColor; + + /* + level = {levelU, levelV}, levelU defines the patch's u direction edges should be + divided into 2^levelU parts, level V defines the patch's v direction edges should + be divided into 2^levelV parts + */ + protected int[] level; + protected List listOfTriangles; + + /** + * Constructor of Patch. + * + * @param color 4 corner's colors + */ + Patch(float[][] color) + { + cornerColor = color.clone(); + } + + /** + * Get the implicit edge for flag = 1. + * + * @return implicit control points + */ + protected abstract PointF[] getFlag1Edge(); + + /** + * Get the implicit edge for flag = 2. + * + * @return implicit control points + */ + protected abstract PointF[] getFlag2Edge(); + + /** + * Get the implicit edge for flag = 3. + * + * @return implicit control points + */ + protected abstract PointF[] getFlag3Edge(); + + /** + * Get the implicit color for flag = 1. + * + * @return color + */ + protected float[][] getFlag1Color() + { + int numberOfColorComponents = cornerColor[0].length; + float[][] implicitCornerColor = new float[2][numberOfColorComponents]; + for (int i = 0; i < numberOfColorComponents; i++) + { + implicitCornerColor[0][i] = cornerColor[1][i]; + implicitCornerColor[1][i] = cornerColor[2][i]; + } + return implicitCornerColor; + } + + /** + * Get implicit color for flag = 2. + * + * @return color + */ + protected float[][] getFlag2Color() + { + int numberOfColorComponents = cornerColor[0].length; + float[][] implicitCornerColor = new float[2][numberOfColorComponents]; + for (int i = 0; i < numberOfColorComponents; i++) + { + implicitCornerColor[0][i] = cornerColor[2][i]; + implicitCornerColor[1][i] = cornerColor[3][i]; + } + return implicitCornerColor; + } + + /** + * Get implicit color for flag = 3. + * + * @return color + */ + protected float[][] getFlag3Color() + { + int numberOfColorComponents = cornerColor[0].length; + float[][] implicitCornerColor = new float[2][numberOfColorComponents]; + for (int i = 0; i < numberOfColorComponents; i++) + { + implicitCornerColor[0][i] = cornerColor[3][i]; + implicitCornerColor[1][i] = cornerColor[0][i]; + } + return implicitCornerColor; + } + + /** + * Calculate the distance from point ps to point pe. + * + * @param ps one end of a line + * @param pe the other end of the line + * @return length of the line + */ + protected double getLen(PointF ps, PointF pe) + { + double x = pe.x - ps.x; + double y = pe.y - ps.y; + return Math.sqrt(x * x + y * y); + } + + /** + * Whether the for control points are on a line. + * + * @param ctl an edge's control points, the size of ctl is 4 + * @return true when 4 control points are on a line, otherwise false + */ + protected boolean isEdgeALine(PointF[] ctl) + { + double ctl1 = Math.abs(edgeEquationValue(ctl[1], ctl[0], ctl[3])); + double ctl2 = Math.abs(edgeEquationValue(ctl[2], ctl[0], ctl[3])); + double x = Math.abs(ctl[0].x - ctl[3].x); + double y = Math.abs(ctl[0].y - ctl[3].y); + return (ctl1 <= x && ctl2 <= x) || (ctl1 <= y && ctl2 <= y); + } + + /** + * A line from point p1 to point p2 defines an equation, adjust the form of + * the equation to let the rhs equals 0, then calculate the lhs value by + * plugging the coordinate of p in the lhs expression. + * + * @param p target point + * @param p1 one end of a line + * @param p2 the other end of a line + * @return calculated value + */ + protected double edgeEquationValue(PointF p, PointF p1, PointF p2) + { + return (p2.y - p1.y) * (p.x - p1.x) - (p2.x - p1.x) * (p.y - p1.y); + } + + /** + * An assistant method to accomplish type 6 and type 7 shading. + * + * @param patchCC all the crossing point coordinates and color of a grid + * @return a ShadedTriangle list which can compose the grid patch + */ + protected List getShadedTriangles(CoordinateColorPair[][] patchCC) + { + List list = new ArrayList(); + int szV = patchCC.length; + int szU = patchCC[0].length; + for (int i = 1; i < szV; i++) + { + for (int j = 1; j < szU; j++) + { + PointF p0 = patchCC[i - 1][j - 1].coordinate; + PointF p1 = patchCC[i - 1][j].coordinate; + PointF p2 = patchCC[i][j].coordinate; + PointF p3 = patchCC[i][j - 1].coordinate; + boolean ll = true; + if (overlaps(p0, p1) || overlaps(p0, p3)) + { + ll = false; + } + else + { + // p0, p1 and p3 are in counter clock wise order, p1 has priority over p0, p3 has priority over p1 + PointF[] llCorner = + { + p0, p1, p3 + }; + float[][] llColor = + { + patchCC[i - 1][j - 1].color, patchCC[i - 1][j].color, patchCC[i][j - 1].color + }; + ShadedTriangle tmpll = new ShadedTriangle(llCorner, llColor); // lower left triangle + list.add(tmpll); + } + if (ll && (overlaps(p2, p1) || overlaps(p2, p3))) + { + } + else + { + // p3, p1 and p2 are in counter clock wise order, p1 has priority over p3, p2 has priority over p1 + PointF[] urCorner = + { + p3, p1, p2 + }; + float[][] urColor = + { + patchCC[i][j - 1].color, patchCC[i - 1][j].color, patchCC[i][j].color + }; + ShadedTriangle tmpur = new ShadedTriangle(urCorner, urColor); // upper right triangle + list.add(tmpur); + } + } + } + return list; + } + + // whether two points p0 and p1 are degenerated into one point within the coordinates' accuracy 0.001 + private boolean overlaps(PointF p0, PointF p1) + { + return Math.abs(p0.x - p1.x) < 0.001 && Math.abs(p0.y - p1.y) < 0.001; + } +} diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/PatchMeshesShadingContext.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/PatchMeshesShadingContext.java new file mode 100644 index 00000000..fd060f44 --- /dev/null +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/PatchMeshesShadingContext.java @@ -0,0 +1,63 @@ +package com.tom_roush.pdfbox.pdmodel.graphics.shading; + +import android.graphics.Point; +import android.graphics.Rect; + +import com.tom_roush.harmony.awt.geom.AffineTransform; +import com.tom_roush.pdfbox.util.Matrix; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class PatchMeshesShadingContext extends TriangleBasedShadingContext{ + + /** + * patch list + */ + private List patchList; + + /** + * Constructor creates an instance to be used for fill operations. + * + * @param shading the shading type to be used + * @param xform transformation for user to device space + * @param matrix the pattern matrix concatenated with that of the parent content stream + * @param deviceBounds device bounds + * @param controlPoints number of control points, 12 for type 6 shading and 16 for type 7 shading + * @throws IOException if something went wrong + */ + protected PatchMeshesShadingContext(PDMeshBasedShadingType shading, + AffineTransform xform, Matrix matrix, Rect deviceBounds, + int controlPoints) throws IOException + { + super(shading, xform, matrix); + patchList = shading.collectPatches(xform, matrix, controlPoints); + createPixelTable(deviceBounds); + } + + @Override + protected Map calcPixelTable(Rect deviceBounds) throws IOException + { + Map map = new HashMap(); + for (Patch it : patchList) + { + super.calcPixelTable(it.listOfTriangles, map, deviceBounds); + } + return map; + } + + @Override + public void dispose() + { + patchList = null; + super.dispose(); + } + + @Override + protected boolean isDataEmpty() + { + return patchList.isEmpty(); + } +} diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/RadialShadingContext.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/RadialShadingContext.java new file mode 100644 index 00000000..e2b28021 --- /dev/null +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/RadialShadingContext.java @@ -0,0 +1,313 @@ +package com.tom_roush.pdfbox.pdmodel.graphics.shading; + +import android.graphics.Bitmap; +import android.graphics.Color; +import android.graphics.Rect; +import android.util.Log; + +import com.tom_roush.harmony.awt.geom.AffineTransform; +import com.tom_roush.pdfbox.cos.COSArray; +import com.tom_roush.pdfbox.cos.COSBoolean; +import com.tom_roush.pdfbox.rendering.PaintContext; +import com.tom_roush.pdfbox.util.Matrix; + +import java.io.IOException; + +public class RadialShadingContext extends ShadingContext implements PaintContext { + + private PDShadingType3 radialShadingType; + + private final float[] coords; + private final float[] domain; + private final boolean[] extend; + private final double x1x0; + private final double y1y0; + private final double r1r0; + private final double r0pow2; + private final float d1d0; + private final double denom; + + private final int factor; + private final int[] colorTable; + + private AffineTransform rat; + + + /** + * Constructor creates an instance to be used for fill operations. + * + * @param shading the shading type to be used + * @param xform transformation for user to device space + * @param matrix the pattern matrix concatenated with that of the parent content stream + * @param deviceBounds the bounds of the area to paint, in device units + * @throws IOException if there is an error getting the color space or doing color conversion. + */ + public RadialShadingContext(PDShadingType3 shading, AffineTransform xform, Matrix matrix, Rect deviceBounds) + throws IOException + { + super(shading, xform, matrix); + this.radialShadingType = shading; + coords = shading.getCoords().toFloatArray(); + + // domain values + if (this.radialShadingType.getDomain() != null) + { + domain = shading.getDomain().toFloatArray(); + } + else + { + // set default values + domain = new float[] { 0, 1 }; + } + + // extend values + COSArray extendValues = shading.getExtend(); + if (extendValues != null) + { + extend = new boolean[2]; + extend[0] = ((COSBoolean) extendValues.getObject(0)).getValue(); + extend[1] = ((COSBoolean) extendValues.getObject(1)).getValue(); + } + else + { + // set default values + extend = new boolean[] { false, false }; + } + // calculate some constants to be used in getRaster + x1x0 = coords[3] - coords[0]; + y1y0 = coords[4] - coords[1]; + r1r0 = coords[5] - coords[2]; + r0pow2 = Math.pow(coords[2], 2); + denom = Math.pow(x1x0, 2) + Math.pow(y1y0, 2) - Math.pow(r1r0, 2); + d1d0 = domain[1] - domain[0]; + + try + { + // get inverse transform to be independent of current user / device space + // when handling actual pixels in getRaster() + rat = matrix.createAffineTransform().createInverse(); + rat.concatenate(xform.createInverse()); + } + catch (AffineTransform.NoninvertibleTransformException ex) + { + Log.e("Pdfbox-Android", ex.getMessage() + ", matrix: " + matrix, ex); + rat = new AffineTransform(); + } + + // shading space -> device space + AffineTransform shadingToDevice = (AffineTransform)xform.clone(); + shadingToDevice.concatenate(matrix.createAffineTransform()); + + // worst case for the number of steps is opposite diagonal corners, so use that + double dist = Math.sqrt(Math.pow(deviceBounds.right - deviceBounds.left, 2) + + Math.pow(deviceBounds.bottom - deviceBounds.top, 2)); + factor = (int) Math.ceil(dist); + + // build the color table for the given number of steps + colorTable = calcColorTable(); + } + + /** + * Calculate the color on the line that connects two circles' centers and store the result in an + * array. + * + * @return an array, index denotes the relative position, the corresponding value the color + */ + private int[] calcColorTable() throws IOException + { + int[] map = new int[factor + 1]; + if (factor == 0 || d1d0 == 0) + { + float[] values = radialShadingType.evalFunction(domain[0]); + map[0] = convertToRGB(values); + } + else + { + for (int i = 0; i <= factor; i++) + { + float t = domain[0] + d1d0 * i / factor; + float[] values = radialShadingType.evalFunction(t); + map[i] = convertToRGB(values); + } + } + return map; + } + + @Override + public void dispose() { + super.dispose(); + radialShadingType = null; + } + + @Override + public Bitmap.Config getColorModel() { + return super.getColorModel(); + } + + @Override + public Bitmap getRaster(int x, int y, int w, int h) { + // create writable raster + Bitmap raster = Bitmap.createBitmap(w, h, getColorModel()); + int[] data = new int[w * h]; + float inputValue = -1; + boolean useBackground; + float[] values = new float[2]; + for (int j = 0; j < h; j++) + { + for (int i = 0; i < w; i++) + { + values[0] = x + i; + values[1] = y + j; + rat.transform(values, 0, values, 0, 1); + useBackground = false; + float[] inputValues = calculateInputValues(values[0], values[1]); + if (Float.isNaN(inputValues[0]) && Float.isNaN(inputValues[1])) + { + if (getBackground() == null) + { + continue; + } + useBackground = true; + } + else + { + // choose 1 of the 2 values + if (inputValues[0] >= 0 && inputValues[0] <= 1) + { + // both values are in the range -> choose the larger one + if (inputValues[1] >= 0 && inputValues[1] <= 1) + { + inputValue = Math.max(inputValues[0], inputValues[1]); + } + // first value is in the range, the second not -> choose first value + else + { + inputValue = inputValues[0]; + } + } + else + { + // first value is not in the range, + // but the second -> choose second value + if (inputValues[1] >= 0 && inputValues[1] <= 1) + { + inputValue = inputValues[1]; + } + // both are not in the range + else + { + if (extend[0] && extend[1]) + { + inputValue = Math.max(inputValues[0], inputValues[1]); + } + else if (extend[0]) + { + inputValue = inputValues[0]; + } + else if (extend[1]) + { + inputValue = inputValues[1]; + } + else if (getBackground() != null) + { + useBackground = true; + } + else + { + continue; + } + } + } + // input value is out of range + if (inputValue > 1) + { + // extend shading if extend[1] is true and nonzero radius + if (extend[1] && coords[5] > 0) + { + inputValue = 1; + } + else + { + if (getBackground() == null) + { + continue; + } + useBackground = true; + } + } + // input value is out of range + else if (inputValue < 0) + { + // extend shading if extend[0] is true and nonzero radius + if (extend[0] && coords[2] > 0) + { + inputValue = 0; + } + else + { + if (getBackground() == null) + { + continue; + } + useBackground = true; + } + } + } + int value; + if (useBackground) + { + // use the given background color values + value = getRgbBackground(); + } + else + { + int key = (int) (inputValue * factor); + value = colorTable[key]; + } + int index = j * w + i; + int r = value & 255; + value >>= 8; + int g = value & 255; + value >>= 8; + int b = value & 255; + data[index] = Color.argb(255, r, g, b); + } + } + raster.setPixels(data, 0, w, 0, 0, w, h); + return raster; + } + + + private float[] calculateInputValues(double x, double y) + { + // According to Adobes Technical Note #5600 we have to do the following + // + // x0, y0, r0 defines the start circle x1, y1, r1 defines the end circle + // + // The parametric equations for the center and radius of the gradient fill circle moving + // between the start circle and the end circle as a function of s are as follows: + // + // xc(s) = x0 + s * (x1 - x0) yc(s) = y0 + s * (y1 - y0) r(s) = r0 + s * (r1 - r0) + // + // Given a geometric coordinate position (x, y) in or along the gradient fill, the + // corresponding value of s can be determined by solving the quadratic constraint equation: + // + // [x - xc(s)]2 + [y - yc(s)]2 = [r(s)]2 + // + // The following code calculates the 2 possible values of s + // + double p = -(x - coords[0]) * x1x0 - (y - coords[1]) * y1y0 - coords[2] * r1r0; + double q = (Math.pow(x - coords[0], 2) + Math.pow(y - coords[1], 2) - r0pow2); + double root = Math.sqrt(p * p - denom * q); + float root1 = (float) ((-p + root) / denom); + float root2 = (float) ((-p - root) / denom); + if (denom < 0) + { + return new float[] { root1, root2 }; + } + else + { + return new float[] { root2, root1 }; + } + } +} diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/RadialShadingPaint.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/RadialShadingPaint.java new file mode 100644 index 00000000..e56af0f1 --- /dev/null +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/RadialShadingPaint.java @@ -0,0 +1,35 @@ +package com.tom_roush.pdfbox.pdmodel.graphics.shading; + +import android.graphics.Rect; +import android.util.Log; + +import com.tom_roush.harmony.awt.geom.AffineTransform; +import com.tom_roush.pdfbox.rendering.PaintContext; +import com.tom_roush.pdfbox.util.Matrix; + +import java.io.IOException; + +public class RadialShadingPaint extends ShadingPaint { + + /** + * Constructor. + * + * @param shading the shading resources + * @param matrix the pattern matrix concatenated with that of the parent content stream + */ + RadialShadingPaint(PDShadingType3 shading, Matrix matrix) + { + super(shading, matrix); + } + + @Override + public PaintContext createContext(Rect deviceBounds, AffineTransform xform) { + try { + return new RadialShadingContext(shading, xform, matrix, deviceBounds); + } + catch (IOException e){ + Log.e("Pdfbox-Android", "IOError while create paint context"); + return null; + } + } +} diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/ShadedTriangle.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/ShadedTriangle.java new file mode 100644 index 00000000..ce77eb06 --- /dev/null +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/ShadedTriangle.java @@ -0,0 +1,248 @@ +package com.tom_roush.pdfbox.pdmodel.graphics.shading; + +import android.graphics.Point; +import android.graphics.PointF; + +import java.util.HashSet; +import java.util.Set; + +public class ShadedTriangle { + + protected final PointF[] corner; // vertices coordinates of a triangle + protected final float[][] color; + private final double area; // area of the triangle + + /* + degree = 3 describes a normal triangle, + degree = 2 when a triangle degenerates to a line, + degree = 1 when a triangle degenerates to a point + */ + private final int degree; + + // describes a rasterized line when a triangle degerates to a line, otherwise null + private final Line line; + + // corner's edge (the opposite edge of a corner) equation value + private final double v0; + private final double v1; + private final double v2; + + /** + * Constructor. + * + * @param p an array of the 3 vertices of a triangle; the PointF objects should not be modified + * by the caller. + * @param c an array of color corresponding the vertex array p + */ + ShadedTriangle(PointF[] p, float[][] c) + { + corner = p.clone(); // yes this is a shallow clone + color = c.clone(); + area = getArea(p[0], p[1], p[2]); + degree = calcDeg(p); + + if (degree == 2) + { + if (overlaps(corner[1], corner[2]) && !overlaps(corner[0], corner[2])) + { + Point p0 = new Point((int) Math.round(corner[0].x), + (int) Math.round(corner[0].x)); + Point p1 = new Point((int) Math.round(corner[2].x), + (int) Math.round(corner[2].y)); + line = new Line(p0, p1, color[0], color[2]); + } + else + { + Point p0 = new Point((int) Math.round(corner[1].x), + (int) Math.round(corner[1].x)); + Point p1 = new Point((int) Math.round(corner[2].x), + (int) Math.round(corner[2].y)); + line = new Line(p0, p1, color[1], color[2]); + } + } + else + { + line = null; + } + + v0 = edgeEquationValue(p[0], p[1], p[2]); + v1 = edgeEquationValue(p[1], p[2], p[0]); + v2 = edgeEquationValue(p[2], p[0], p[1]); + } + + /** + * Calculate the degree value of a triangle. + * + * @param p 3 vertices coordinates + * @return number of unique points in the 3 vertices of a triangle, 3, 2 or + * 1 + */ + private int calcDeg(PointF[] p) + { + Set set = new HashSet(); + for (PointF itp : p) + { + Point np = new Point((int) Math.round(itp.x * 1000), (int) Math.round(itp.y * 1000)); + set.add(np); + } + return set.size(); + } + + public int getDeg() + { + return degree; + } + + /** + * get the boundary of a triangle. + * + * @return {xmin, xmax, ymin, ymax} + */ + public int[] getBoundary() + { + int[] boundary = new int[4]; + int x0 = (int) Math.round(corner[0].x); + int x1 = (int) Math.round(corner[1].x); + int x2 = (int) Math.round(corner[2].x); + int y0 = (int) Math.round(corner[0].y); + int y1 = (int) Math.round(corner[1].y); + int y2 = (int) Math.round(corner[2].y); + + boundary[0] = Math.min(Math.min(x0, x1), x2); + boundary[1] = Math.max(Math.max(x0, x1), x2); + boundary[2] = Math.min(Math.min(y0, y1), y2); + boundary[3] = Math.max(Math.max(y0, y1), y2); + + return boundary; + } + + /** + * Get the line of a triangle. + * + * @return points of the line, or null if this triangle isn't a line + */ + public Line getLine() + { + return line; + } + + /** + * Whether a point is contained in this ShadedTriangle. + * + * @param p the target point + * @return false if p is outside of this triangle, otherwise true + */ + public boolean contains(PointF p) + { + if (degree == 1) + { + return overlaps(corner[0], p) || overlaps(corner[1], p) || overlaps(corner[2], p); + } + else if (degree == 2) + { + Point tp = new Point((int) Math.round(p.x), (int) Math.round(p.y)); + return line.linePoints.contains(tp); + } + + /* + the following code judges whether a point is contained in a normal triangle, + taking the on edge case as contained + */ + double pv0 = edgeEquationValue(p, corner[1], corner[2]); + /* + if corner[0] and point p are on different sides of line from corner[1] to corner[2], + p is outside of the triangle + */ + if (pv0 * v0 < 0) + { + return false; + } + double pv1 = edgeEquationValue(p, corner[2], corner[0]); + /* + if vertex corner[1] and point p are on different sides of line from corner[2] to corner[0], + p is outside of the triangle + */ + if (pv1 * v1 < 0) + { + return false; + } + double pv2 = edgeEquationValue(p, corner[0], corner[1]); + /* + only left one case: + if corner[1] and point p are on different sides of line from corner[2] to corner[0], + p is outside of the triangle, otherwise p is contained in the triangle + */ + return pv2 * v2 >= 0; // !(pv2 * v2 < 0) + } + + /* + check whether two points overlaps each other, as points' coordinates are + of type double, the coordinates' accuracy used here is 0.001 + */ + private boolean overlaps(PointF p0, PointF p1) + { + return Math.abs(p0.x - p1.x) < 0.001 && Math.abs(p0.y - p1.y) < 0.001; + } + + /* + two points can define a line equation, adjust the form of the equation to + let the rhs equals 0, calculate the lhs value by plugging the coordinate + of p in the lhs expression + */ + private double edgeEquationValue(PointF p, PointF p1, PointF p2) + { + return (p2.y - p1.y) * (p.x - p1.x) + - (p2.x - p1.x) * (p.y - p1.y); + } + + // calculate the area of a triangle + private double getArea(PointF a, PointF b, PointF c) + { + return Math.abs((c.x - b.x) * (c.y - a.y) + - (c.x - a.x) * (c.y - b.y)) / 2.0; + } + + /** + * Calculate the color of a point. + * + * @param p the target point + * @return an array denotes the point's color + */ + public float[] calcColor(PointF p) + { + int numberOfColorComponents = color[0].length; + float[] pCol = new float[numberOfColorComponents]; + + switch (degree) + { + case 1: + for (int i = 0; i < numberOfColorComponents; i++) + { + // average + pCol[i] = (color[0][i] + color[1][i] + color[2][i]) / 3.0f; + } + break; + case 2: + // linear interpolation + Point tp = new Point((int) Math.round(p.x), (int) Math.round(p.y)); + return line.calcColor(tp); + default: + float aw = (float) (getArea(p, corner[1], corner[2]) / area); + float bw = (float) (getArea(p, corner[2], corner[0]) / area); + float cw = (float) (getArea(p, corner[0], corner[1]) / area); + for (int i = 0; i < numberOfColorComponents; i++) + { + // barycentric interpolation + pCol[i] = color[0][i] * aw + color[1][i] * bw + color[2][i] * cw; + } + break; + } + return pCol; + } + + @Override + public String toString() + { + return corner[0] + " " + corner[1] + " " + corner[2]; + } +} diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/ShadingContext.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/ShadingContext.java new file mode 100644 index 00000000..ed019993 --- /dev/null +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/ShadingContext.java @@ -0,0 +1,96 @@ +package com.tom_roush.pdfbox.pdmodel.graphics.shading; + +import android.graphics.Bitmap; +import android.graphics.ColorSpace; + +import com.tom_roush.harmony.awt.geom.AffineTransform; +import com.tom_roush.pdfbox.cos.COSArray; +import com.tom_roush.pdfbox.pdmodel.graphics.color.PDColorSpace; +import com.tom_roush.pdfbox.util.Matrix; + +import java.io.IOException; + +public abstract class ShadingContext { + private float[] background; + private int rgbBackground; + private final PDShading shading; + private Bitmap.Config outputColorModel; + private PDColorSpace shadingColorSpace; + + /** + * Constructor. + * + * @param shading the shading type to be used + * @param xform transformation for user to device space + * @param matrix the pattern matrix concatenated with that of the parent content stream + * @throws java.io.IOException if there is an error getting the color space + * or doing background color conversion. + */ + public ShadingContext(PDShading shading, AffineTransform xform, Matrix matrix) throws IOException + { + this.shading = shading; + shadingColorSpace = shading.getColorSpace(); + + // create the output color model using RGB+alpha as color space + outputColorModel = Bitmap.Config.ARGB_8888; + + // get background values if available + COSArray bg = shading.getBackground(); + if (bg != null) + { + background = bg.toFloatArray(); + rgbBackground = convertToRGB(background); + } + } + + PDColorSpace getShadingColorSpace() + { + return shadingColorSpace; + } + + PDShading getShading() + { + return shading; + } + + float[] getBackground() + { + return background; + } + + int getRgbBackground() + { + return rgbBackground; + } + + /** + * Convert color values from shading colorspace to RGB color values encoded + * into an integer. + * + * @param values color values in shading colorspace. + * @return RGB values encoded in an integer. + * @throws java.io.IOException if the color conversion fails. + */ + final int convertToRGB(float[] values) throws IOException + { + int normRGBValues; + + float[] rgbValues = shadingColorSpace.toRGB(values); + normRGBValues = (int) (rgbValues[0] * 255); + normRGBValues |= (int) (rgbValues[1] * 255) << 8; + normRGBValues |= (int) (rgbValues[2] * 255) << 16; + + return normRGBValues; + } + + Bitmap.Config getColorModel() + { + return outputColorModel; + } + + void dispose() + { + outputColorModel = null; + shadingColorSpace = null; + } +} diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/ShadingPaint.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/ShadingPaint.java new file mode 100644 index 00000000..52ac298d --- /dev/null +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/ShadingPaint.java @@ -0,0 +1,35 @@ +package com.tom_roush.pdfbox.pdmodel.graphics.shading; + + +import android.graphics.Paint; + +import com.tom_roush.pdfbox.rendering.WrapPaint; +import com.tom_roush.pdfbox.util.Matrix; + +public abstract class ShadingPaint implements WrapPaint { + + protected T shading; + protected final Matrix matrix; + + ShadingPaint(T shading, Matrix matrix) + { + this.shading = shading; + this.matrix = matrix; + } + + /** + * @return the PDShading of this paint + */ + public T getShading() + { + return shading; + } + + /** + * @return the active Matrix of this paint + */ + public Matrix getMatrix() + { + return matrix; + } +} diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/TensorPatch.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/TensorPatch.java new file mode 100644 index 00000000..6ce34a62 --- /dev/null +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/TensorPatch.java @@ -0,0 +1,253 @@ +package com.tom_roush.pdfbox.pdmodel.graphics.shading; + +import android.graphics.PointF; + +import java.util.List; + +public class TensorPatch extends Patch{ + /** + * Constructor of a patch for type 7 shading. + * + * @param tcp 16 control points + * @param color 4 corner colors + */ + protected TensorPatch(PointF[] tcp, float[][] color) + { + super(color); + controlPoints = reshapeControlPoints(tcp); + level = calcLevel(); + listOfTriangles = getTriangles(); + } + + /* + order the 16 1d points to a square matrix which is as the one described + in p.199 of PDF3200_2008.pdf rotated 90 degrees clockwise + */ + private PointF[][] reshapeControlPoints(PointF[] tcp) + { + PointF[][] square = new PointF[4][4]; + for (int i = 0; i <= 3; i++) + { + square[0][i] = tcp[i]; + square[3][i] = tcp[9 - i]; + } + for (int i = 1; i <= 2; i++) + { + square[i][0] = tcp[12 - i]; + square[i][2] = tcp[12 + i]; + square[i][3] = tcp[3 + i]; + } + square[1][1] = tcp[12]; + square[2][1] = tcp[15]; + return square; + } + + // calculate the dividing level from the control points + private int[] calcLevel() + { + int[] l = + { + 4, 4 + }; + + PointF[] ctlC1 = new PointF[4]; + PointF[] ctlC2 = new PointF[4]; + for (int j = 0; j < 4; j++) + { + ctlC1[j] = controlPoints[j][0]; + ctlC2[j] = controlPoints[j][3]; + } + // if two opposite edges are both lines, there is a possibility to reduce the dividing level + if (isEdgeALine(ctlC1) && isEdgeALine(ctlC2)) + { + /* + if any of the 4 inner control points is out of the patch formed by the 4 edges, + keep the high dividing level, + otherwise, determine the dividing level by the lengths of edges + */ + if (isOnSameSideCC(controlPoints[1][1]) || isOnSameSideCC(controlPoints[1][2]) + || isOnSameSideCC(controlPoints[2][1]) || isOnSameSideCC(controlPoints[2][2])) + { + // keep the high dividing level + } + else + { + // length's unit is one pixel in device space + double lc1 = getLen(ctlC1[0], ctlC1[3]), lc2 = getLen(ctlC2[0], ctlC2[3]); + if (lc1 > 800 || lc2 > 800) + { + // keeps init value 4 + } + else if (lc1 > 400 || lc2 > 400) + { + l[0] = 3; + } + else if (lc1 > 200 || lc2 > 200) + { + l[0] = 2; + } + else + { + l[0] = 1; + } + } + } + + // the other two opposite edges + if (isEdgeALine(controlPoints[0]) && isEdgeALine(controlPoints[3])) + { + if (isOnSameSideDD(controlPoints[1][1]) || isOnSameSideDD(controlPoints[1][2]) + || isOnSameSideDD(controlPoints[2][1]) || isOnSameSideDD(controlPoints[2][2])) + { + // keep the high dividing level + } + else + { + double ld1 = getLen(controlPoints[0][0], controlPoints[0][3]); + double ld2 = getLen(controlPoints[3][0], controlPoints[3][3]); + if (ld1 > 800 || ld2 > 800) + { + // keeps init value 4 + } + else if (ld1 > 400 || ld2 > 400) + { + l[1] = 3; + } + else if (ld1 > 200 || ld2 > 200) + { + l[1] = 2; + } + else + { + l[1] = 1; + } + } + } + return l; + } + + // whether a point is on the same side of edge C1 and edge C2 + private boolean isOnSameSideCC(PointF p) + { + double cc = edgeEquationValue(p, controlPoints[0][0], controlPoints[3][0]) + * edgeEquationValue(p, controlPoints[0][3], controlPoints[3][3]); + return cc > 0; + } + + // whether a point is on the same side of edge D1 and edge D2 + private boolean isOnSameSideDD(PointF p) + { + double dd = edgeEquationValue(p, controlPoints[0][0], controlPoints[0][3]) + * edgeEquationValue(p, controlPoints[3][0], controlPoints[3][3]); + return dd > 0; + } + + // get a list of triangles which compose this tensor patch + private List getTriangles() + { + CoordinateColorPair[][] patchCC = getPatchCoordinatesColor(); + return getShadedTriangles(patchCC); + } + + @Override + protected PointF[] getFlag1Edge() + { + PointF[] implicitEdge = new PointF[4]; + for (int i = 0; i < 4; i++) + { + implicitEdge[i] = controlPoints[i][3]; + } + return implicitEdge; + } + + @Override + protected PointF[] getFlag2Edge() + { + PointF[] implicitEdge = new PointF[4]; + for (int i = 0; i < 4; i++) + { + implicitEdge[i] = controlPoints[3][3 - i]; + } + return implicitEdge; + } + + @Override + protected PointF[] getFlag3Edge() + { + PointF[] implicitEdge = new PointF[4]; + for (int i = 0; i < 4; i++) + { + implicitEdge[i] = controlPoints[3 - i][0]; + } + return implicitEdge; + } + + /* + dividing a patch into a grid according to level, then calculate the coordinate and color of + each crossing point in the grid, the rule to calculate the coordinate is tensor-product which + is defined in page 119 of PDF32000_2008.pdf, the method to calculate the corresponding color is + bilinear interpolation + */ + private CoordinateColorPair[][] getPatchCoordinatesColor() + { + int numberOfColorComponents = cornerColor[0].length; + double[][] bernsteinPolyU = getBernsteinPolynomials(level[0]); + int szU = bernsteinPolyU[0].length; + double[][] bernsteinPolyV = getBernsteinPolynomials(level[1]); + int szV = bernsteinPolyV[0].length; + CoordinateColorPair[][] patchCC = new CoordinateColorPair[szV][szU]; + + double stepU = 1.0 / (szU - 1); + double stepV = 1.0 / (szV - 1); + double v = -stepV; + for (int k = 0; k < szV; k++) + { + // v and u are the assistant parameters + v += stepV; + double u = -stepU; + for (int l = 0; l < szU; l++) + { + double tmpx = 0.0; + double tmpy = 0.0; + // these two "for" loops are for the equation to define the patch surface (coordinates) + for (int i = 0; i < 4; i++) + { + for (int j = 0; j < 4; j++) + { + tmpx += controlPoints[i][j].x * bernsteinPolyU[i][l] * bernsteinPolyV[j][k]; + tmpy += controlPoints[i][j].y * bernsteinPolyU[i][l] * bernsteinPolyV[j][k]; + } + } + PointF tmpC = new PointF((float) tmpx, (float) tmpy); + + u += stepU; + float[] paramSC = new float[numberOfColorComponents]; + for (int ci = 0; ci < numberOfColorComponents; ci++) + { + paramSC[ci] = (float) ((1 - v) * ((1 - u) * cornerColor[0][ci] + u * cornerColor[3][ci]) + + v * ((1 - u) * cornerColor[1][ci] + u * cornerColor[2][ci])); // bilinear interpolation + } + patchCC[k][l] = new CoordinateColorPair(tmpC, paramSC); + } + } + return patchCC; + } + + // Bernstein polynomials which are defined in page 119 of PDF32000_2008.pdf + private double[][] getBernsteinPolynomials(int lvl) + { + int sz = (1 << lvl) + 1; + double[][] poly = new double[4][sz]; + double step = 1.0 / (sz - 1); + double t = -step; + for (int i = 0; i < sz; i++) + { + t += step; + poly[0][i] = (1 - t) * (1 - t) * (1 - t); + poly[1][i] = 3 * t * (1 - t) * (1 - t); + poly[2][i] = 3 * t * t * (1 - t); + poly[3][i] = t * t * t; + } + return poly; + } +} diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/TriangleBasedShadingContext.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/TriangleBasedShadingContext.java new file mode 100644 index 00000000..5a58fb1d --- /dev/null +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/TriangleBasedShadingContext.java @@ -0,0 +1,179 @@ +package com.tom_roush.pdfbox.pdmodel.graphics.shading; + +import android.graphics.Bitmap; +import android.graphics.Color; +import android.graphics.Point; +import android.graphics.PointF; +import android.graphics.Rect; + +import com.tom_roush.harmony.awt.geom.AffineTransform; +import com.tom_roush.pdfbox.rendering.PaintContext; +import com.tom_roush.pdfbox.util.Matrix; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +abstract class TriangleBasedShadingContext extends ShadingContext implements PaintContext { + + // map of pixels within triangles to their RGB color + private Map pixelTable; + /** + * Constructor. + * + * @param shading the shading type to be used + * @param xform transformation for user to device space + * @param matrix the pattern matrix concatenated with that of the parent content stream + * @throws IOException if there is an error getting the color space + * or doing background color conversion. + */ + public TriangleBasedShadingContext(PDShading shading, AffineTransform xform, Matrix matrix) throws IOException { + super(shading, xform, matrix); + } + + /** + * Creates the pixel table. + */ + protected final void createPixelTable(Rect deviceBounds) throws IOException + { + pixelTable = calcPixelTable(deviceBounds); + } + + /** + * Calculate every point and its color and store them in a Hash table. + * + * @return a Hash table which contains all the points' positions and colors of one image + */ + abstract Map calcPixelTable(Rect deviceBounds) throws IOException; + + /** + * Get the points from the triangles, calculate their color and add point-color mappings. + */ + protected void calcPixelTable(List triangleList, Map map, + Rect deviceBounds) throws IOException + { + for (ShadedTriangle tri : triangleList) + { + int degree = tri.getDeg(); + if (degree == 2) + { + Line line = tri.getLine(); + for (Point p : line.linePoints) + { + map.put(p, evalFunctionAndConvertToRGB(line.calcColor(p))); + } + } + else + { + int[] boundary = tri.getBoundary(); + boundary[0] = Math.max(boundary[0], deviceBounds.left); + boundary[1] = Math.min(boundary[1], deviceBounds.right); + boundary[2] = Math.max(boundary[2], deviceBounds.top); + boundary[3] = Math.min(boundary[3], deviceBounds.bottom); + for (int x = boundary[0]; x <= boundary[1]; x++) + { + for (int y = boundary[2]; y <= boundary[3]; y++) + { + Point p = new Point(x, y); + PointF pf = new PointF(x, y); + if (tri.contains(pf)) + { + map.put(p, evalFunctionAndConvertToRGB(tri.calcColor(pf))); + } + } + } + + // "fatten" triangle by drawing the borders with Bresenham's line algorithm + // Inspiration: Raph Levien in http://bugs.ghostscript.com/show_bug.cgi?id=219588 + Point p0 = new Point((int) Math.round(tri.corner[0].x), + (int) Math.round(tri.corner[0].y)); + Point p1 = new Point((int) Math.round(tri.corner[1].x), + (int) Math.round(tri.corner[1].y)); + Point p2 = new Point((int) Math.round(tri.corner[2].x), + (int) Math.round(tri.corner[2].y)); + Line l1 = new Line(p0, p1, tri.color[0], tri.color[1]); + Line l2 = new Line(p1, p2, tri.color[1], tri.color[2]); + Line l3 = new Line(p2, p0, tri.color[2], tri.color[0]); + for (Point p : l1.linePoints) + { + map.put(p, evalFunctionAndConvertToRGB(l1.calcColor(p))); + } + for (Point p : l2.linePoints) + { + map.put(p, evalFunctionAndConvertToRGB(l2.calcColor(p))); + } + for (Point p : l3.linePoints) + { + map.put(p, evalFunctionAndConvertToRGB(l3.calcColor(p))); + } + } + } + } + + /** + * Convert color to RGB color value, using function if required, then convert from the shading + * color space to an RGB value, which is encoded into an integer. + */ + private int evalFunctionAndConvertToRGB(float[] values) throws IOException + { + if (getShading().getFunction() != null) + { + values = getShading().evalFunction(values); + } + return convertToRGB(values); + } + + /** + * Returns true if the shading has an empty data stream. + */ + abstract boolean isDataEmpty(); + + @Override + public void dispose() { + super.dispose(); + } + + @Override + public Bitmap.Config getColorModel() { + return super.getColorModel(); + } + + @Override + public Bitmap getRaster(int x, int y, int w, int h) { + Bitmap raster = Bitmap.createBitmap(w, h, getColorModel()); + int[] data = new int[w * h]; + if (!isDataEmpty() || getBackground() != null) + { + for (int row = 0; row < h; row++) + { + for (int col = 0; col < w; col++) + { + Point p = new Point(x + col, y + row); + int value; + Integer v = pixelTable.get(p); + if (v != null) + { + value = v; + } + else + { + if (getBackground() == null) + { + continue; + } + value = getRgbBackground(); + } + int index = row * w + col; + int r = value & 255; + value >>= 8; + int g = value & 255; + value >>= 8; + int b = value & 255; + data[index] = Color.argb(255, r, g, b); + } + } + } + raster.setPixels(data, 0, w, 0, 0, w, h); + return raster; + } +} diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/Type1ShadingContext.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/Type1ShadingContext.java new file mode 100644 index 00000000..7c49ef33 --- /dev/null +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/Type1ShadingContext.java @@ -0,0 +1,117 @@ +package com.tom_roush.pdfbox.pdmodel.graphics.shading; + +import android.graphics.Bitmap; +import android.graphics.Color; +import android.util.Log; + +import com.tom_roush.harmony.awt.geom.AffineTransform; +import com.tom_roush.pdfbox.pdmodel.graphics.color.PDColorSpace; +import com.tom_roush.pdfbox.rendering.PaintContext; +import com.tom_roush.pdfbox.util.Matrix; + +import java.io.IOException; + +public class Type1ShadingContext extends ShadingContext implements PaintContext { + + private PDShadingType1 type1ShadingType; + private AffineTransform rat; + private final float[] domain; + + /** + * Constructor. + * + * @param shading the shading type to be used + * @param xform transformation for user to device space + * @param matrix the pattern matrix concatenated with that of the parent content stream + * @throws IOException if there is an error getting the color space + * or doing background color conversion. + */ + public Type1ShadingContext(PDShadingType1 shading, AffineTransform xform, Matrix matrix) throws IOException { + super(shading, xform, matrix); + this.type1ShadingType = shading; + + // (Optional) An array of four numbers [ xmin xmax ymin ymax ] + // specifying the rectangular domain of coordinates over which the + // color function(s) are defined. Default value: [ 0.0 1.0 0.0 1.0 ]. + if (shading.getDomain() != null) { + domain = shading.getDomain().toFloatArray(); + } else { + domain = new float[]{0, 1, 0, 1}; + } + + try { + // get inverse transform to be independent of + // shading matrix and current user / device space + // when handling actual pixels in getRaster() + rat = shading.getMatrix().createAffineTransform().createInverse(); + rat.concatenate(matrix.createAffineTransform().createInverse()); + rat.concatenate(xform.createInverse()); + } catch (AffineTransform.NoninvertibleTransformException ex) { + Log.e("Pdfbox-Android", ex.getMessage() + ", matrix: " + matrix, ex); + rat = new AffineTransform(); + } + } + + @Override + public void dispose() { + type1ShadingType = null; + } + + @Override + public Bitmap.Config getColorModel() { + return super.getColorModel(); + } + + @Override + public Bitmap getRaster(int x, int y, int w, int h) { + Bitmap raster = Bitmap.createBitmap(w, h, getColorModel()); + int[] data = new int[w * h]; + float[] values = new float[2]; + for (int j = 0; j < h; j++) { + for (int i = 0; i < w; i++) { + int index = j * w + i; + boolean useBackground = false; + values[0] = x + i; + values[1] = y + j; + rat.transform(values, 0, values, 0, 1); + if (values[0] < domain[0] || values[0] > domain[1] || + values[1] < domain[2] || values[1] > domain[3]) { + if (getBackground() == null) { + continue; + } + useBackground = true; + } + + // evaluate function + float[] tmpValues; // "values" can't be reused due to different length + if (useBackground) { + tmpValues = getBackground(); + } else { + try { + tmpValues = type1ShadingType.evalFunction(values); + } catch (IOException e) { + Log.e("Pdfbox-Android", "error while processing a function", e); + continue; + } + } + + // convert color values from shading color space to RGB + PDColorSpace shadingColorSpace = getShadingColorSpace(); + if (shadingColorSpace != null) { + try { + tmpValues = shadingColorSpace.toRGB(tmpValues); + } catch (IOException e) { + Log.e("Pdfbox-Android", "error processing color space", e); + continue; + } + } + int r = (int) (tmpValues[0] * 255); + int g = (int) (tmpValues[1] * 255); + int b = (int) (tmpValues[2] * 255); + data[index] = Color.argb(255, r, g, b); + } + } + raster.setPixels(data, 0, w, 0, 0, w, h); + return raster; + } +} diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/Type1ShadingPaint.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/Type1ShadingPaint.java new file mode 100644 index 00000000..d3084589 --- /dev/null +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/Type1ShadingPaint.java @@ -0,0 +1,37 @@ +package com.tom_roush.pdfbox.pdmodel.graphics.shading; + +import android.graphics.Rect; +import android.util.Log; + +import com.tom_roush.harmony.awt.geom.AffineTransform; +import com.tom_roush.pdfbox.rendering.PaintContext; +import com.tom_roush.pdfbox.util.Matrix; + +import java.io.IOException; + +public class Type1ShadingPaint extends ShadingPaint { + + /** + * Constructor. + * + * @param shading the shading resources + * @param matrix the pattern matrix concatenated with that of the parent content stream + */ + Type1ShadingPaint(PDShadingType1 shading, Matrix matrix) { + super(shading, matrix); + } + + public int getTransparency() { + return 0; + } + + @Override + public PaintContext createContext(Rect deviceBounds, AffineTransform xform) { + try { + return new Type1ShadingContext(shading, xform, matrix); + } catch (IOException e) { + Log.e("Pdfbox-Android", "An error occurred while painting", e); + return null; + } + } +} diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/Type4ShadingContext.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/Type4ShadingContext.java new file mode 100644 index 00000000..8787aca4 --- /dev/null +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/Type4ShadingContext.java @@ -0,0 +1,33 @@ +package com.tom_roush.pdfbox.pdmodel.graphics.shading; + +import android.graphics.Rect; +import android.util.Log; + +import com.tom_roush.harmony.awt.geom.AffineTransform; +import com.tom_roush.pdfbox.util.Matrix; + +import java.io.IOException; + +public class Type4ShadingContext extends GouraudShadingContext{ + private final int bitsPerFlag; + + /** + * Constructor creates an instance to be used for fill operations. + * + * @param shading the shading type to be used + * @param xform transformation for user to device space + * @param matrix the pattern matrix concatenated with that of the parent content stream + */ + Type4ShadingContext(PDShadingType4 shading, AffineTransform xform, + Matrix matrix, Rect deviceBounds) throws IOException + { + super(shading, xform, matrix); + Log.d("Pdfbox-Android", "Type4ShadingContext"); + + bitsPerFlag = shading.getBitsPerFlag(); + //TODO handle cases where bitperflag isn't 8 + Log.d("Pdfbox-Android", "bitsPerFlag: " + bitsPerFlag); + setTriangleList(shading.collectTriangles(xform, matrix)); + createPixelTable(deviceBounds); + } +} diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/Type4ShadingPaint.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/Type4ShadingPaint.java new file mode 100644 index 00000000..6903196a --- /dev/null +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/Type4ShadingPaint.java @@ -0,0 +1,35 @@ +package com.tom_roush.pdfbox.pdmodel.graphics.shading; + +import android.graphics.Rect; +import android.util.Log; + +import com.tom_roush.harmony.awt.geom.AffineTransform; +import com.tom_roush.pdfbox.rendering.PaintContext; +import com.tom_roush.pdfbox.util.Matrix; + +import java.io.IOException; + +public class Type4ShadingPaint extends ShadingPaint{ + + /** + * Constructor. + * + * @param shading the shading resources + * @param matrix the pattern matrix concatenated with that of the parent content stream + */ + Type4ShadingPaint(PDShadingType4 shading, Matrix matrix) + { + super(shading, matrix); + } + + @Override + public PaintContext createContext(Rect deviceBounds, AffineTransform xform) { + try { + return new Type4ShadingContext(shading, xform, matrix, deviceBounds); + } + catch (IOException e){ + Log.e("Pdfbox-Android", "IOError while create paint context"); + return null; + } + } +} diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/Type5ShadingContext.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/Type5ShadingContext.java new file mode 100644 index 00000000..49b71cab --- /dev/null +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/Type5ShadingContext.java @@ -0,0 +1,28 @@ +package com.tom_roush.pdfbox.pdmodel.graphics.shading; + +import android.graphics.Rect; +import android.util.Log; + +import com.tom_roush.harmony.awt.geom.AffineTransform; +import com.tom_roush.pdfbox.util.Matrix; + +import java.io.IOException; + +public class Type5ShadingContext extends GouraudShadingContext{ + /** + * Constructor creates an instance to be used for fill operations. + * + * @param shading the shading type to be used + * @param xform transformation for user to device space + * @param matrix the pattern matrix concatenated with that of the parent content stream + * @throws IOException if something went wrong + */ + protected Type5ShadingContext(PDShadingType5 shading, AffineTransform xform, Matrix matrix, Rect deviceBounds) throws IOException { + super(shading, xform, matrix); + + Log.d("Pdfbox-Android", "Type5ShadingContext"); + + setTriangleList(shading.collectTriangles(xform, matrix)); + createPixelTable(deviceBounds); + } +} diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/Type5ShadingPaint.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/Type5ShadingPaint.java new file mode 100644 index 00000000..fc937a81 --- /dev/null +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/Type5ShadingPaint.java @@ -0,0 +1,35 @@ +package com.tom_roush.pdfbox.pdmodel.graphics.shading; + +import android.graphics.Rect; +import android.util.Log; + +import com.tom_roush.harmony.awt.geom.AffineTransform; +import com.tom_roush.pdfbox.rendering.PaintContext; +import com.tom_roush.pdfbox.util.Matrix; + +import java.io.IOException; + +public class Type5ShadingPaint extends ShadingPaint{ + + /** + * Constructor. + * + * @param shading the shading resources + * @param matrix the pattern matrix concatenated with that of the parent content stream + */ + Type5ShadingPaint(PDShadingType5 shading, Matrix matrix) + { + super(shading, matrix); + } + + @Override + public PaintContext createContext(Rect deviceBounds, AffineTransform xform) { + try { + return new Type5ShadingContext(shading, xform, matrix, deviceBounds); + } + catch (IOException e){ + Log.e("Pdfbox-Android", "IOError while create paint context"); + return null; + } + } +} diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/Type6ShadingContext.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/Type6ShadingContext.java new file mode 100644 index 00000000..d4126706 --- /dev/null +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/Type6ShadingContext.java @@ -0,0 +1,23 @@ +package com.tom_roush.pdfbox.pdmodel.graphics.shading; + +import android.graphics.Rect; + +import com.tom_roush.harmony.awt.geom.AffineTransform; +import com.tom_roush.pdfbox.util.Matrix; + +import java.io.IOException; + +public class Type6ShadingContext extends PatchMeshesShadingContext{ + /** + * Constructor creates an instance to be used for fill operations. + * + * @param shading the shading type to be used + * @param xform transformation for user to device space + * @param matrix the pattern matrix concatenated with that of the parent content stream + * @param deviceBounds device bounds + * @throws IOException if something went wrong + */ + protected Type6ShadingContext(PDShadingType6 shading, AffineTransform xform, Matrix matrix, Rect deviceBounds) throws IOException { + super(shading, xform, matrix, deviceBounds, 12); + } +} diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/Type6ShadingPaint.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/Type6ShadingPaint.java new file mode 100644 index 00000000..f9e7c43e --- /dev/null +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/Type6ShadingPaint.java @@ -0,0 +1,36 @@ +package com.tom_roush.pdfbox.pdmodel.graphics.shading; + +import android.graphics.Rect; +import android.util.Log; + +import com.tom_roush.harmony.awt.geom.AffineTransform; +import com.tom_roush.pdfbox.rendering.PaintContext; +import com.tom_roush.pdfbox.util.Matrix; + +import java.io.IOException; + +public class Type6ShadingPaint extends ShadingPaint{ + + /** + * Constructor. + * + * @param shading the shading resources + * @param matrix the pattern matrix concatenated with that of the parent content stream + */ + Type6ShadingPaint(PDShadingType6 shading, Matrix matrix) + { + super(shading, matrix); + } + + + @Override + public PaintContext createContext(Rect deviceBounds, AffineTransform xform) { + try { + return new Type6ShadingContext(shading, xform, matrix, deviceBounds); + } + catch (IOException e){ + Log.e("Pdfbox-Android", "IOError while create paint context"); + return null; + } + } +} diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/Type7ShadingContext.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/Type7ShadingContext.java new file mode 100644 index 00000000..0ce2fd5e --- /dev/null +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/Type7ShadingContext.java @@ -0,0 +1,23 @@ +package com.tom_roush.pdfbox.pdmodel.graphics.shading; + +import android.graphics.Rect; + +import com.tom_roush.harmony.awt.geom.AffineTransform; +import com.tom_roush.pdfbox.util.Matrix; + +import java.io.IOException; + +public class Type7ShadingContext extends PatchMeshesShadingContext { + /** + * Constructor creates an instance to be used for fill operations. + * + * @param shading the shading type to be used + * @param xform transformation for user to device space + * @param matrix the pattern matrix concatenated with that of the parent content stream + * @param deviceBounds device bounds + * @throws IOException if something went wrong + */ + protected Type7ShadingContext(PDShadingType7 shading, AffineTransform xform, Matrix matrix, Rect deviceBounds) throws IOException { + super(shading, xform, matrix, deviceBounds, 16); + } +} diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/Type7ShadingPaint.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/Type7ShadingPaint.java new file mode 100644 index 00000000..c91d88bc --- /dev/null +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/Type7ShadingPaint.java @@ -0,0 +1,35 @@ +package com.tom_roush.pdfbox.pdmodel.graphics.shading; + +import android.graphics.Rect; +import android.util.Log; + +import com.tom_roush.harmony.awt.geom.AffineTransform; +import com.tom_roush.pdfbox.rendering.PaintContext; +import com.tom_roush.pdfbox.util.Matrix; + +import java.io.IOException; + +public class Type7ShadingPaint extends ShadingPaint{ + + /** + * Constructor. + * + * @param shading the shading resources + * @param matrix the pattern matrix concatenated with that of the parent content stream + */ + Type7ShadingPaint(PDShadingType7 shading, Matrix matrix) + { + super(shading, matrix); + } + + @Override + public PaintContext createContext(Rect deviceBounds, AffineTransform xform) { + try { + return new Type7ShadingContext(shading, xform, matrix, deviceBounds); + } + catch (IOException e){ + Log.e("Pdfbox-Android", "IOError while create paint context"); + return null; + } + } +} diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/Vertex.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/Vertex.java new file mode 100644 index 00000000..8c764dc2 --- /dev/null +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/Vertex.java @@ -0,0 +1,30 @@ +package com.tom_roush.pdfbox.pdmodel.graphics.shading; + +import android.graphics.PointF; + +class Vertex { + + public PointF point; + public float[] color; + + Vertex(PointF p, float[] c) + { + point = p; + color = c.clone(); + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + for (float f : color) + { + if (sb.length() > 0) + { + sb.append(' '); + } + sb.append(String.format("%3.2f", f)); + } + return "Vertex{ " + point + ", colors=[" + sb + "] }"; + } +} diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/state/PDGraphicsState.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/state/PDGraphicsState.java index 5aa34396..e45800f4 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/state/PDGraphicsState.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/state/PDGraphicsState.java @@ -29,6 +29,7 @@ import com.tom_roush.pdfbox.pdmodel.graphics.color.PDColor; import com.tom_roush.pdfbox.pdmodel.graphics.color.PDColorSpace; import com.tom_roush.pdfbox.pdmodel.graphics.color.PDDeviceGray; +import com.tom_roush.pdfbox.util.GraphicsUtil; import com.tom_roush.pdfbox.util.Matrix; /** @@ -76,13 +77,7 @@ public class PDGraphicsState implements Cloneable */ public PDGraphicsState(PDRectangle page) { -// clippingPath = new Area(new GeneralPath(page.toGeneralPath()));TODO: PdfBox-Android - RectF bounds = new RectF(); - page.toGeneralPath().computeBounds(bounds, true); - clippingPath = new Region(); - Rect boundsRounded = new Rect(); - bounds.round(boundsRounded); - clippingPath.setPath(page.toGeneralPath(), new Region(boundsRounded)); + clippingPath = GraphicsUtil.getPathRegion(page.toGeneralPath()); } /** @@ -600,25 +595,16 @@ public void setNonStrokingColorSpace(PDColorSpace colorSpace) */ public void intersectClippingPath(Path path) { - RectF bounds = new RectF(); - path.computeBounds(bounds, true); - Region r = new Region(); - Rect boundsRounded = new Rect(); - bounds.round(boundsRounded); - r.setPath(path, new Region(boundsRounded)); - intersectClippingPath(r); - // TODO: PdfBox-Android Verify correct behavior + intersectClippingPath(GraphicsUtil.getPathRegion(path)); } /** * Modify the current clipping path by intersecting it with the given path. * @param area area to intersect with the clipping path */ - public void intersectClippingPath(Region area) - { + public void intersectClippingPath(Region area) { // lazy cloning of clipping path for performance - if (!isClippingPathDirty) - { + if (!isClippingPathDirty) { // deep copy (can't use clone() as it performs only a shallow copy) Region cloned = new Region(area); // cloned.add(clippingPath); @@ -636,8 +622,7 @@ public void intersectClippingPath(Region area) * * @return The current clipping path. */ - public Region getCurrentClippingPath() - { + public Region getCurrentClippingPath(){ return clippingPath; } diff --git a/library/src/main/java/com/tom_roush/pdfbox/rendering/PageDrawer.java b/library/src/main/java/com/tom_roush/pdfbox/rendering/PageDrawer.java index 865dcf56..37ff7e5b 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/rendering/PageDrawer.java +++ b/library/src/main/java/com/tom_roush/pdfbox/rendering/PageDrawer.java @@ -19,10 +19,13 @@ import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; +import android.graphics.ColorMatrix; +import android.graphics.ColorMatrixColorFilter; import android.graphics.DashPathEffect; import android.graphics.Paint; import android.graphics.Path; import android.graphics.PointF; +import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Region; import android.util.Log; @@ -62,12 +65,16 @@ import com.tom_roush.pdfbox.pdmodel.graphics.color.PDColor; import com.tom_roush.pdfbox.pdmodel.graphics.color.PDColorSpace; import com.tom_roush.pdfbox.pdmodel.graphics.color.PDDeviceGray; +import com.tom_roush.pdfbox.pdmodel.graphics.color.PDPattern; import com.tom_roush.pdfbox.pdmodel.graphics.form.PDFormXObject; import com.tom_roush.pdfbox.pdmodel.graphics.form.PDTransparencyGroup; import com.tom_roush.pdfbox.pdmodel.graphics.image.PDImage; import com.tom_roush.pdfbox.pdmodel.graphics.optionalcontent.PDOptionalContentGroup; import com.tom_roush.pdfbox.pdmodel.graphics.optionalcontent.PDOptionalContentGroup.RenderState; import com.tom_roush.pdfbox.pdmodel.graphics.optionalcontent.PDOptionalContentMembershipDictionary; +import com.tom_roush.pdfbox.pdmodel.graphics.pattern.PDAbstractPattern; +import com.tom_roush.pdfbox.pdmodel.graphics.pattern.PDShadingPattern; +import com.tom_roush.pdfbox.pdmodel.graphics.pattern.PDTilingPattern; import com.tom_roush.pdfbox.pdmodel.graphics.shading.PDShading; import com.tom_roush.pdfbox.pdmodel.graphics.state.PDExtendedGraphicsState; import com.tom_roush.pdfbox.pdmodel.graphics.state.PDGraphicsState; @@ -77,6 +84,7 @@ import com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDAnnotation; import com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDAnnotationUnknown; import com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDAppearanceDictionary; +import com.tom_roush.pdfbox.util.GraphicsUtil; import com.tom_roush.pdfbox.util.Matrix; import com.tom_roush.pdfbox.util.Vector; @@ -104,6 +112,8 @@ public class PageDrawer extends PDFGraphicsStreamEngine private Paint paint; private Canvas canvas; private AffineTransform xform; + private double xformScalingFactorX; + private double xformScalingFactorY; // the page box to draw (usually the crop box but may be another) private PDRectangle pageSize; @@ -118,6 +128,7 @@ public class PageDrawer extends PDFGraphicsStreamEngine // last clipping path private Region lastClip; + private int lastStackSize = 0; // clip when drawPage() is called, can be null, must be intersected when clipping private Path initialClip; @@ -216,10 +227,6 @@ protected final Path getLinePath() */ private void setRenderingHints() { -// graphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION, -// RenderingHints.VALUE_INTERPOLATION_BICUBIC); -// graphics.setRenderingHint(RenderingHints.KEY_RENDERING, -// RenderingHints.VALUE_RENDER_QUALITY); paint.setAntiAlias(true); } @@ -236,7 +243,10 @@ public void drawPage(Paint p, Canvas c, PDRectangle pageSize) throws IOException paint = p; canvas = c; xform = new AffineTransform(canvas.getMatrix()); -// initialClip = graphics.getClip(); TODO: PdfBox-Android + xformScalingFactorX = Math.abs(xform.getScalingFactorX()); + xformScalingFactorY = Math.abs(xform.getScalingFactorY()); + // backup init status + canvas.save(); this.pageSize = pageSize; setRenderingHints(); @@ -246,7 +256,6 @@ public void drawPage(Paint p, Canvas c, PDRectangle pageSize) throws IOException // adjust for non-(0,0) crop box canvas.translate(-pageSize.getLowerLeftX(), -pageSize.getLowerLeftY()); - canvas.save(); processPage(getPage()); @@ -254,10 +263,46 @@ public void drawPage(Paint p, Canvas c, PDRectangle pageSize) throws IOException { showAnnotation(annotation); } + canvas.restore(); } -// void drawTilingPattern(Graphics2D g, PDTilingPattern pattern, PDColorSpace colorSpace, -// PDColor color, Matrix patternMatrix) throws IOException TODO: PdfBox-Android + /** + * Draws the pattern stream to the requested context. + * @param c The graphics context to draw onto. + * @param pattern The tiling pattern to be used. + * @param colorSpace color space for this tiling. + * @param color color for this tiling. + * @param patternMatrix the pattern matrix + * @throws IOException If there is an IO error while drawing the page. + */ + void drawTilingPattern(Canvas c, PDTilingPattern pattern, PDColorSpace colorSpace, + PDColor color, Matrix patternMatrix) throws IOException{ + Canvas savedGraphics = canvas; + canvas = c; + + Path savedLinePath = linePath; + linePath = new Path(); + Path.FillType savedClipWindingRule = clipWindingRule; + clipWindingRule = Path.FillType.WINDING; + + Region savedLastClips = lastClip; + lastClip = null; + Path savedInitialClip = initialClip; + initialClip = null; + + boolean savedFlipTG = flipTG; + flipTG = true; + + setRenderingHints(); + processTilingPattern(pattern, color, colorSpace, patternMatrix); + + flipTG = savedFlipTG; + canvas = savedGraphics; + linePath = savedLinePath; + lastClip = savedLastClips; + initialClip = savedInitialClip; + clipWindingRule = savedClipWindingRule; + } private float clampColor(float color) { @@ -284,7 +329,16 @@ private void setClip() Region clippingPath = getGraphicsState().getCurrentClippingPath(); if (clippingPath != lastClip) { -// canvas.clipPath(clippingPath.getBoundaryPath()); TODO: PdfBox-Android + // android canvas manage clips with save/restore in a private stack, we can not + // modify clip casually, so we store current stack size in `lastStackSize` after setting clip, + // and restore it before next setting + if(lastStackSize >= 1){ + canvas.restoreToCount(lastStackSize); + } + lastStackSize = canvas.save(); + if(!clippingPath.isEmpty()){ + canvas.clipPath(clippingPath.getBoundaryPath()); + } if (initialClip != null) { // apply the remembered initial clip, but transform it first @@ -388,31 +442,25 @@ private void drawGlyph2D(Glyph2D glyph2D, PDFont font, int code, Vector displace } // render glyph -// Shape glyph = at.createTransformedShape(path); + Path glyph = at.createTransformedShape(path); path.transform(at.toMatrix()); - if (renderingMode.isFill()) - { + if(isContentRendered()) { + if (renderingMode.isFill()) { // graphics.setComposite(state.getNonStrokingJavaComposite()); // graphics.setPaint(getNonStrokingPaint()); - paint.setColor(getNonStrokingColor()); - setClip(); - if (isContentRendered()) - { + paint.setColor(getNonStrokingColor()); + setClip(); paint.setStyle(Paint.Style.FILL); canvas.drawPath(path, paint); } - } - if (renderingMode.isStroke()) - { + if (renderingMode.isStroke()) { // graphics.setComposite(state.getStrokingJavaComposite()); // graphics.setPaint(getStrokingPaint()); - paint.setColor(getStrokingColor()); -// graphics.setStroke(getStroke()); - setClip(); - if (isContentRendered()) - { + paint.setColor(getStrokingColor()); + setStroke(); + setClip(); paint.setStyle(Paint.Style.STROKE); canvas.drawPath(path, paint); } @@ -420,7 +468,7 @@ private void drawGlyph2D(Glyph2D glyph2D, PDFont font, int code, Vector displace if (renderingMode.isClip()) { -// textClippings.add(glyph); TODO: PdfBox-Android + textClippings.add(glyph); } } } @@ -517,11 +565,86 @@ public void appendRectangle(PointF p0, PointF p1, PointF p2, PointF p3) linePath.close(); } -// private Paint applySoftMaskToPaint(Paint parentPaint, PDSoftMask softMask) throws IOException TODO: Pdfbox-Android + private WrapPaint applySoftMaskToPaint(WrapPaint parentPaint, PDSoftMask softMask) throws IOException{ + + if (softMask == null || softMask.getGroup() == null) + { + return parentPaint; + } + PDColor backdropColor = null; + if (COSName.LUMINOSITY.equals(softMask.getSubType())) + { + COSArray backdropColorArray = softMask.getBackdropColor(); + if (backdropColorArray != null) + { + PDTransparencyGroup form = softMask.getGroup(); + PDColorSpace colorSpace = form.getGroup().getColorSpace(form.getResources()); + if (colorSpace != null) + { + backdropColor = new PDColor(backdropColorArray, colorSpace); + } + } + } + TransparencyGroup transparencyGroup = new TransparencyGroup(softMask.getGroup(), true, + softMask.getInitialTransformationMatrix(), backdropColor); + Bitmap image = transparencyGroup.getImage(); + if (image == null) + { + // Adobe Reader ignores empty softmasks instead of using bc color + // sample file: PDFJS-6967_reduced_outside_softmask.pdf + return parentPaint; + } + Bitmap gray; + if (COSName.ALPHA.equals(softMask.getSubType())) + { + gray = image.extractAlpha(); + } + else if (COSName.LUMINOSITY.equals(softMask.getSubType())) + { + // convert color bitmap to gray bitmap + gray = Bitmap.createBitmap(image.getWidth(), image.getHeight(), Bitmap.Config.RGB_565); + Canvas c = new Canvas(gray); + Paint paint = new Paint(); + ColorMatrix cm = new ColorMatrix(); + cm.setSaturation(0); + ColorMatrixColorFilter f = new ColorMatrixColorFilter(cm); + paint.setColorFilter(f); + c.drawBitmap(image, 0, 0, paint); + } + else + { + throw new IOException("Invalid soft mask subtype."); + } + gray = adjustImage(gray); + RectF tpgBounds = transparencyGroup.getBounds(); + return new SoftMask(parentPaint, gray, tpgBounds, backdropColor, softMask.getTransferFunction()); + } // private void adjustRectangle(RectF r) TODO: PdfBox-Android -// private Bitmap adjustImage(Bitmap gray) TODO: PdfBox-Android + private Bitmap adjustImage(Bitmap gray) throws IOException { + AffineTransform at = new AffineTransform(xform); + at.scale(1.0 / xformScalingFactorX, 1.0 / xformScalingFactorY); + + RectF originalBounds = new RectF(0, 0, gray.getWidth(), gray.getHeight()); + RectF transformedBounds = new RectF(); + Path orgPath = new Path(); + orgPath.addRect(originalBounds, Path.Direction.CW); + at.createTransformedShape(orgPath).computeBounds(transformedBounds, true); + at.preConcatenate(AffineTransform.getTranslateInstance(-transformedBounds.left, -transformedBounds.top)); + + int width = (int) Math.ceil(transformedBounds.width()); + int height = (int) Math.ceil(transformedBounds.height()); + + if (width == gray.getWidth() && height == gray.getHeight() && at.isIdentity()) { + return gray; + } + + Bitmap transformedGray = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565); + Canvas g2 = new Canvas(transformedGray); + g2.drawBitmap(gray, at.toMatrix(), null); + return transformedGray; + } // private Paint getStrokingPaint() throws IOException TODO: PdfBox-Android @@ -618,16 +741,14 @@ private float[] getDashArray(PDLineDashPattern dashPattern) } @Override - public void strokePath() throws IOException - { -// graphics.setComposite(getGraphicsState().getStrokingJavaComposite()); - setStroke(); - paint.setStyle(Paint.Style.STROKE); - paint.setColor(getStrokingColor()); - setClip(); + public void strokePath() throws IOException { //TODO bbox of shading pattern should be used here? (see fillPath) - if (isContentRendered()) - { + if (isContentRendered()) { +// graphics.setComposite(getGraphicsState().getStrokingJavaComposite()); + setStroke(); + paint.setStyle(Paint.Style.STROKE); + paint.setColor(getStrokingColor()); + setClip(); canvas.drawPath(linePath, paint); } linePath.reset(); @@ -636,6 +757,7 @@ public void strokePath() throws IOException @Override public void fillPath(Path.FillType windingRule) throws IOException { + PDGraphicsState graphicsState = getGraphicsState(); // graphics.setComposite(getGraphicsState().getNonStrokingJavaComposite()); paint.setColor(getNonStrokingColor()); setClip(); @@ -654,10 +776,27 @@ public void fillPath(Path.FillType windingRule) throws IOException paint.setAntiAlias(false); } - if (isContentRendered()) + Path shape; + if (graphicsState.getNonStrokingColorSpace() instanceof PDPattern) + { + // apply clip to path to avoid oversized device bounds in shading contexts (PDFBOX-2901) + Region area = GraphicsUtil.getPathRegion(linePath); + Rect clip = canvas.getClipBounds(); + if (clip != null) + { + area.op(clip, Region.Op.INTERSECT); + } + intersectShadingBBox(graphicsState.getNonStrokingColor(), area); + shape = area.getBoundaryPath(); + } + else + { + shape = linePath; + } + if (isContentRendered() && !shape.isEmpty()) { paint.setStyle(Paint.Style.FILL); - canvas.drawPath(linePath, paint); + canvas.drawPath(shape, paint); } linePath.reset(); @@ -670,7 +809,29 @@ public void fillPath(Path.FillType windingRule) throws IOException } } -// private void intersectShadingBBox(PDColor color, Area area) throws IOException TODO: PdfBox-Android + + // checks whether this is a shading pattern and if yes, + // get the transformed BBox and intersect with current paint area + // need to do it here and not in shading getRaster() because it may have been rotated + private void intersectShadingBBox(PDColor color, Region area) throws IOException + { + if (color.getColorSpace() instanceof PDPattern) + { + PDColorSpace colorSpace = color.getColorSpace(); + PDAbstractPattern pat = ((PDPattern) colorSpace).getPattern(color); + if (pat instanceof PDShadingPattern) + { + PDShading shading = ((PDShadingPattern) pat).getShading(); + PDRectangle bbox = shading.getBBox(); + if (bbox != null) + { + Matrix m = Matrix.concatenate(getInitialMatrix(), pat.getMatrix()); + Region bboxArea = GraphicsUtil.getPathRegion(bbox.transform(m)); + area.op(bboxArea, Region.Op.INTERSECT); + } + } + } + } /** * Returns true if the given path is rectangular. @@ -740,17 +901,19 @@ public void closePath() @Override public void endPath() { -// if (clipWindingRule != null) -// { -// linePath.setFillType(clipWindingRule); -// getGraphicsState().intersectClippingPath(linePath); -// -// // PDFBOX-3836: lastClip needs to be reset, because after intersection it is still the same -// // object, thus setClip() would believe that it is cached. -// lastClip = null; -// -// clipWindingRule = null; -// } TODO: PdfBox-Android causes rendering issues + if (clipWindingRule != null) + { + linePath.setFillType(clipWindingRule); + + if(!linePath.isEmpty()) { + getGraphicsState().intersectClippingPath(linePath); + } + // PDFBOX-3836: lastClip needs to be reset, because after intersection it is still the same + // object, thus setClip() would believe that it is cached. + lastClip = null; + + clipWindingRule = null; + } linePath.reset(); } @@ -875,10 +1038,13 @@ private void drawBitmap(Bitmap image, AffineTransform at) throws IOException } } - private Bitmap applyTransferFunction(Bitmap image, COSBase transfer) throws IOException - { - Bitmap bim = Bitmap.createBitmap(image.getWidth(), image.getHeight(), Bitmap.Config.ARGB_8888); + private Bitmap applyTransferFunction(Bitmap image, COSBase transfer) throws IOException { + int width = image.getWidth(); + int height = image.getHeight(); // TODO: Pdfbox-Android - does this always need to be ARGB_8888? + Bitmap bim = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + int[] outPixels = new int[width]; + int[] imgPixels = new int[width]; // prepare transfer functions (either one per color or one for all) // and maps (actually arrays[256] to be faster) to avoid calculating values several times @@ -888,8 +1054,7 @@ private Bitmap applyTransferFunction(Bitmap image, COSBase transfer) throws IOEx PDFunction rf; PDFunction gf; PDFunction bf; - if (transfer instanceof COSArray) - { + if (transfer instanceof COSArray) { COSArray ar = (COSArray) transfer; rf = PDFunction.create(ar.getObject(0)); gf = PDFunction.create(ar.getObject(1)); @@ -897,9 +1062,7 @@ private Bitmap applyTransferFunction(Bitmap image, COSBase transfer) throws IOEx rMap = new Integer[256]; gMap = new Integer[256]; bMap = new Integer[256]; - } - else - { + } else { rf = PDFunction.create(transfer); gf = rf; bf = rf; @@ -910,49 +1073,40 @@ private Bitmap applyTransferFunction(Bitmap image, COSBase transfer) throws IOEx // apply the transfer function to each color, but keep alpha float[] input = new float[1]; - for (int x = 0; x < image.getWidth(); ++x) - { - for (int y = 0; y < image.getHeight(); ++y) - { - int rgb = image.getPixel(x, y); + for (int y = 0; y < height; ++y) { + image.getPixels(imgPixels, 0, width, 0, y, width, 1); + for (int x = 0; x < width; ++x) { + int rgb = imgPixels[x]; int ri = (rgb >> 16) & 0xFF; int gi = (rgb >> 8) & 0xFF; int bi = rgb & 0xFF; int ro; int go; int bo; - if (rMap[ri] != null) - { + if (rMap[ri] != null) { ro = rMap[ri]; - } - else - { + } else { input[0] = (ri & 0xFF) / 255f; ro = (int) (rf.eval(input)[0] * 255); rMap[ri] = ro; } - if (gMap[gi] != null) - { + if (gMap[gi] != null) { go = gMap[gi]; - } - else - { + } else { input[0] = (gi & 0xFF) / 255f; go = (int) (gf.eval(input)[0] * 255); gMap[gi] = go; } - if (bMap[bi] != null) - { + if (bMap[bi] != null) { bo = bMap[bi]; - } - else - { + } else { input[0] = (bi & 0xFF) / 255f; bo = (int) (bf.eval(input)[0] * 255); bMap[bi] = bo; } - bim.setPixel(x, y, (rgb & 0xFF000000) | (ro << 16) | (go << 8) | bo); + outPixels[x] = (rgb & 0xFF000000) | (ro << 16) | (go << 8) | bo; } + bim.setPixels(outPixels, 0, width, 0, y, width, 1); } return bim; } @@ -967,27 +1121,50 @@ public void shadingFill(COSName shadingName) throws IOException return; } Matrix ctm = getGraphicsState().getCurrentTransformationMatrix(); -// Paint paint = shading.toPaint(ctm); -// paint = applySoftMaskToPaint(paint, getGraphicsState().getSoftMask()); -// graphics.setComposite(getGraphicsState().getNonStrokingJavaComposite()); -// graphics.setPaint(paint); -// graphics.setClip(null); -// lastClip = null; + // graphics.setComposite(getGraphicsState().getNonStrokingJavaComposite()); + // save previous clip + canvas.save(); + lastClip = null; // get the transformed BBox and intersect with current clipping path // need to do it here and not in shading getRaster() because it may have been rotated PDRectangle bbox = shading.getBBox(); - if (bbox != null) - { -// Area bboxArea = new Area(bbox.transform(ctm)); -// bboxArea.intersect(getGraphicsState().getCurrentClippingPath()); -// graphics.fill(bboxArea); + Region area; + if (bbox != null) { + // set max draw area + area = GraphicsUtil.getPathRegion(bbox.transform(ctm)); + area.op(getGraphicsState().getCurrentClippingPath(), Region.Op.INTERSECT); } else { -// graphics.fill(getGraphicsState().getCurrentClippingPath()); + RectF bounds = shading.getBounds(new AffineTransform(), ctm); + if (bounds != null) { + GraphicsUtil.addPoint2Rect(bounds, new PointF((float) Math.floor(bounds.left - 1), + (float) Math.floor(bounds.top - 1))); + GraphicsUtil.addPoint2Rect(bounds, new PointF((float) Math.ceil(bounds.right + 1), + (float) Math.ceil(bounds.bottom + 1))); + area = new Region(); + int right = (int) bounds.right + (bounds.right % 1 > 0 ? 1 : 0); + int bottom = (int) bounds.bottom + (bounds.bottom % 1 > 0 ? 1 : 0); + area.set(new Rect((int) bounds.left, (int) bounds.top, right, bottom)); + } + else + { + area = getGraphicsState().getCurrentClippingPath(); + } } + if(!area.isEmpty()) { + canvas.clipPath(area.getBoundaryPath()); + WrapPaint maskPaint = applySoftMaskToPaint(shading.toPaint(ctm), getGraphicsState().getSoftMask()); + Rect drawArea = area.getBounds(); + PaintContext ctx = maskPaint.createContext(drawArea, xform); + Bitmap bitmap = ctx.getRaster(drawArea.left, drawArea.top, drawArea.width(), drawArea.height()); + Rect src = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight()); + canvas.drawBitmap(bitmap, src, drawArea, null); + } + // restore previous clip + canvas.restore(); } @Override @@ -1049,6 +1226,13 @@ public void showForm(PDFormXObject form) throws IOException { super.showForm(form); } + if (isContentRendered()) + { + Path savedLinePath = linePath; + linePath = new Path(); + super.showForm(form); + linePath = savedLinePath; + } } public void setStroke(Paint p, float width, Paint.Cap cap, Paint.Join join, float miterLimit, float[] dash, float dash_phase) @@ -1147,15 +1331,15 @@ public void showTransparencyGroup(PDTransparencyGroup form) throws IOException **/ private final class TransparencyGroup { -// private final Bitmap image; -// private final PDRectangle bbox; - -// private final int minX; -// private final int minY; -// private final int maxX; -// private final int maxY; -// private final int width; -// private final int height; + private final Bitmap image; + private final PDRectangle bbox; + + private final int minX; + private final int minY; + private final int maxX; + private final int maxY; + private final int width; + private final int height; private final float scaleX; private final float scaleY; @@ -1174,9 +1358,9 @@ private final class TransparencyGroup private TransparencyGroup(PDTransparencyGroup form, boolean isSoftMask, Matrix ctm, PDColor backdropColor) throws IOException { -// Graphics2D savedGraphics = graphics; -// Area savedLastClip = lastClip; -// Shape savedInitialClip = initialClip; + Canvas savedGraphics = canvas; + Region savedLastClip = lastClip; + Path savedInitialClip = initialClip; // get the CTM x Form Matrix transform Matrix transform = Matrix.concatenate(ctm, form.getMatrix()); @@ -1185,49 +1369,46 @@ private TransparencyGroup(PDTransparencyGroup form, boolean isSoftMask, Matrix c Path transformedBox = form.getBBox().transform(transform); // clip the bbox to prevent giant bboxes from consuming all memory -// Area clip = (Area)getGraphicsState().getCurrentClippingPath().clone(); -// clip.intersect(new Area(transformedBox)); -// Rectangle2D clipRect = clip.getBounds2D(); + Region clip = getGraphicsState().getCurrentClippingPath(); + clip.op(GraphicsUtil.getPathRegion(transformedBox), Region.Op.INTERSECT); + Rect clipRect = clip.getBounds(); Matrix m = new Matrix(xform); scaleX = Math.abs(m.getScalingFactorX()); scaleY = Math.abs(m.getScalingFactorY()); -// if (clipRect.isEmpty()) -// { -// image = null; -// bbox = null; -// minX = 0; -// minY = 0; -// maxX = 0; -// maxY = 0; -// width = 0; -// height = 0; -// return; -// } -// this.bbox = new PDRectangle((float)clipRect.getX(), (float)clipRect.getY(), -// (float)clipRect.getWidth(), (float)clipRect.getHeight()); + if (clipRect.isEmpty()) + { + image = null; + bbox = null; + minX = 0; + minY = 0; + maxX = 0; + maxY = 0; + width = 0; + height = 0; + return; + } + this.bbox = new PDRectangle(clipRect.left, clipRect.top, clipRect.width(), clipRect.height()); // apply the underlying Graphics2D device's DPI transform AffineTransform dpiTransform = AffineTransform.getScaleInstance(scaleX, scaleY); -// Rectangle2D bounds = dpiTransform.createTransformedShape(clip.getBounds2D()).getBounds2D(); + RectF bounds = new RectF(); + dpiTransform.createTransformedShape(clip.getBoundaryPath()).computeBounds(bounds, true); -// minX = (int) Math.floor(bounds.getMinX()); -// minY = (int) Math.floor(bounds.getMinY()); -// maxX = (int) Math.floor(bounds.getMaxX()) + 1; -// maxY = (int) Math.floor(bounds.getMaxY()) + 1; + minX = (int) Math.floor(bounds.left); + minY = (int) Math.floor(bounds.top); + maxX = (int) Math.floor(bounds.right) + 1; + maxY = (int) Math.floor(bounds.bottom) + 1; -// width = maxX - minX; -// height = maxY - minY; + width = maxX - minX; + height = maxY - minY; // FIXME - color space - if (isGray(form.getGroup().getColorSpace(form.getResources()))) - { + // ignore gray check temporarily TODO: PdfBox-Android +// if (isGray(form.getGroup().getColorSpace(form.getResources()))) +// { // image = create2ByteGrayAlphaImage(width, height); - } - else - { -// image = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); - } -// Graphics2D g = image.createGraphics(); +// } + image = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); boolean needsBackdrop = !isSoftMask && !form.getGroup().isIsolated() && hasBlendMode(form, new HashSet()); @@ -1241,63 +1422,68 @@ private TransparencyGroup(PDTransparencyGroup form, boolean isSoftMask, Matrix c { // Use the current page as the parent group. backdropImage = renderer.getPageImage(); - needsBackdrop = backdropImage != null; -// backdropX = minX; -// backdropY = (backdropImage != null) ? (backdropImage.getHeight() - maxY) : 0; + if (backdropImage == null) + { + needsBackdrop = false; + } + else + { + backdropX = minX; + backdropY = backdropImage.getHeight() - maxY; + } } else { TransparencyGroup parentGroup = transparencyGroupStack.peek(); -// backdropImage = parentGroup.image; -// backdropX = minX - parentGroup.minX; -// backdropY = parentGroup.maxY - maxY; + backdropImage = parentGroup.image; + backdropX = minX - parentGroup.minX; + backdropY = parentGroup.maxY - maxY; } } -// Graphics2D g = image.createGraphics(); + Canvas g = new Canvas(image); if (needsBackdrop) { // backdropImage must be included in group image but not in group alpha. -// g.drawImage(backdropImage, 0, 0, width, height, -// backdropX, backdropY, backdropX + width, backdropY + height, null); -// g = new GroupGraphics(image, g); + Rect dest = new Rect(0, 0, width, height); + Rect src = new Rect(backdropX, backdropY, backdropX + width, backdropY + height); + g.drawBitmap(backdropImage, src, dest, null); } if (isSoftMask && backdropColor != null) { // "If the subtype is Luminosity, the transparency group XObject G shall be // composited with a fully opaque backdrop whose colour is everywhere defined // by the soft-mask dictionary's BC entry." -// g.setBackground(new Color(backdropColor.toRGB())); -// g.clearRect(0, 0, width, height); + g.drawColor(backdropColor.toRGB()); } // flip y-axis -// g.translate(0, image.getHeight()); -// g.scale(1, -1); + g.translate(0, image.getHeight()); + g.scale(1, -1); boolean savedFlipTG = flipTG; flipTG = false; // apply device transform (DPI) // the initial translation is ignored, because we're not writing into the initial graphics device -// g.transform(dpiTransform); + g.setMatrix(dpiTransform.toMatrix()); AffineTransform xformOriginal = xform; xform = AffineTransform.getScaleInstance(scaleX, scaleY); PDRectangle pageSizeOriginal = pageSize; -// pageSize = new PDRectangle(minX / scaleX, -// minY / scaleY, -// (float) bounds.getWidth() / scaleX, -// (float) bounds.getHeight() / scaleY); + pageSize = new PDRectangle(minX / scaleX, + minY / scaleY, + (float) bounds.width() / scaleX, + (float) bounds.height() / scaleY); Path.FillType clipWindingRuleOriginal = clipWindingRule; clipWindingRule = null; Path linePathOriginal = linePath; linePath = new Path(); // adjust the origin -// g.translate(-clipRect.getX(), -clipRect.getY()); + g.translate(-clipRect.left, -clipRect.top); -// graphics = g; + canvas = g; setRenderingHints(); try { @@ -1314,29 +1500,25 @@ private TransparencyGroup(PDTransparencyGroup form, boolean isSoftMask, Matrix c transparencyGroupStack.pop(); } } + + if (needsBackdrop) + { + //((Canvas) g).removeBackdrop(backdropImage, backdropX, backdropY); TODO: PdfBox-Android + } } finally { flipTG = savedFlipTG; -// lastClip = savedLastClip; -// graphics.dispose(); -// graphics = savedGraphics; -// initialClip = savedInitialClip; + lastClip = savedLastClip; + canvas = savedGraphics; + initialClip = savedInitialClip; clipWindingRule = clipWindingRuleOriginal; linePath = linePathOriginal; pageSize = pageSizeOriginal; xform = xformOriginal; - - if (needsBackdrop) - { -// ((GroupGraphics) g).removeBackdrop(backdropImage, backdropX, backdropY); - } } } - // http://stackoverflow.com/a/21181943/535646 -// private BufferedImage create2ByteGrayAlphaImage(int width, int height) TODO: PdfBox-Android - private boolean isGray(PDColorSpace colorSpace) { if (colorSpace instanceof PDDeviceGray) @@ -1357,27 +1539,27 @@ private boolean isGray(PDColorSpace colorSpace) return false; } -// public Bitmap getImage() -// { -// return image; -// } + public Bitmap getImage() + { + return image; + } -// public PDRectangle getBBox() -// { -// return bbox; -// } + public PDRectangle getBBox() + { + return bbox; + } -// public RectF getBounds() -// { -// PointF size = new PointF(pageSize.getWidth(), pageSize.getHeight()); -// // apply the underlying Graphics2D device's DPI transform and y-axis flip -// AffineTransform dpiTransform = AffineTransform.getScaleInstance(scaleX, scaleY); -// size = dpiTransform.transform(size, size); -// // Flip y -// return new RectF(minX - pageSize.getLowerLeftX() * scaleX, -// size.y - minY - height + pageSize.getLowerLeftY() * scaleY, -// width, height); -// } + public RectF getBounds() + { + PointF size = new PointF(pageSize.getWidth(), pageSize.getHeight()); + // apply the underlying Graphics2D device's DPI transform and y-axis flip + AffineTransform dpiTransform = AffineTransform.getScaleInstance(scaleX, scaleY); + size = dpiTransform.transform(size, size); + // Flip y + return new RectF(minX - pageSize.getLowerLeftX() * scaleX, + size.y - minY - height + pageSize.getLowerLeftY() * scaleY, + width, height); + } } private boolean hasBlendMode(PDTransparencyGroup group, Set groupsDone) diff --git a/library/src/main/java/com/tom_roush/pdfbox/rendering/PaintContext.java b/library/src/main/java/com/tom_roush/pdfbox/rendering/PaintContext.java new file mode 100644 index 00000000..f4035bcd --- /dev/null +++ b/library/src/main/java/com/tom_roush/pdfbox/rendering/PaintContext.java @@ -0,0 +1,38 @@ +package com.tom_roush.pdfbox.rendering; + +import android.graphics.Bitmap; +import android.graphics.ColorSpace; + +public interface PaintContext { + + /** + * Releases the resources allocated for the operation. + */ + public void dispose(); + + /** + * Returns the {@code ColorModel} of the output. Note that + * this {@code ColorModel} might be different from the hint + * specified in the createContext method of + * {@code Paint}. Not all {@code PaintContext} objects are + * capable of generating color patterns in an arbitrary + * {@code ColorModel}. + * @return the {@code ColorModel} of the output. + */ + Bitmap.Config getColorModel(); + + /** + * Returns a {@code Raster} containing the colors generated for + * the graphics operation. + * @param x the x coordinate of the area in device space + * for which colors are generated. + * @param y the y coordinate of the area in device space + * for which colors are generated. + * @param w the width of the area in device space + * @param h the height of the area in device space + * @return a {@code Raster} representing the specified + * rectangular area and containing the colors generated for + * the graphics operation. + */ + Bitmap getRaster(int x, int y, int w, int h); +} diff --git a/library/src/main/java/com/tom_roush/pdfbox/rendering/SoftMask.java b/library/src/main/java/com/tom_roush/pdfbox/rendering/SoftMask.java new file mode 100644 index 00000000..70b22ed1 --- /dev/null +++ b/library/src/main/java/com/tom_roush/pdfbox/rendering/SoftMask.java @@ -0,0 +1,162 @@ +package com.tom_roush.pdfbox.rendering; + +import android.graphics.Bitmap; +import android.graphics.Color; +import android.graphics.Rect; +import android.graphics.RectF; + +import com.tom_roush.harmony.awt.geom.AffineTransform; +import com.tom_roush.pdfbox.pdmodel.common.function.PDFunction; +import com.tom_roush.pdfbox.pdmodel.common.function.PDFunctionTypeIdentity; +import com.tom_roush.pdfbox.pdmodel.graphics.color.PDColor; + +import java.io.IOException; + +public class SoftMask implements WrapPaint { + + private static final Bitmap.Config ARGB_COLOR_MODEL = Bitmap.Config.ARGB_8888; + + private final WrapPaint paint; + private Bitmap mask; + private final RectF bboxDevice; + private int bc = 0; + private final PDFunction transferFunction; + + /** + * Creates a new soft mask paint. + * + * @param paint underlying paint. + * @param mask soft mask + * @param bboxDevice bbox of the soft mask in the underlying Graphics2D device space + * @param backdropColor the color to be used outside the transparency group’s bounding box; if + * null, black will be used. + * @param transferFunction the transfer function, may be null. + */ + SoftMask(WrapPaint paint, Bitmap mask, RectF bboxDevice, PDColor backdropColor, PDFunction transferFunction) { + this.paint = paint; + this.mask = mask; + this.bboxDevice = bboxDevice; + if (transferFunction instanceof PDFunctionTypeIdentity) { + this.transferFunction = null; + } else { + this.transferFunction = transferFunction; + } + if (backdropColor != null) { + try { + int bgColor = backdropColor.toRGB(); + float red = Color.red(bgColor) / 255F; + float green = Color.green(bgColor) / 255F; + float blue = Color.blue(bgColor) / 255F; + // http://stackoverflow.com/a/25463098/535646 + bc = Math.round(299 * red + 587 * green + 114 * blue) / 1000; + } catch (IOException ex) { + // keep default + } + } + } + +// @Override +// public void drawShader(Bitmap origin, Canvas canvas) { +// Rect clipBounds = canvas.getClipBounds(); +// Bitmap masked = new SoftPaintContext(origin).getRaster(clipBounds.left, clipBounds.top, clipBounds.width(), clipBounds.height()); +// Rect src = new Rect(0, 0, masked.getWidth(), masked.getHeight()); +// //paint. +// canvas.drawBitmap(masked, src, new Rect(0, 0, clipBounds.width(), clipBounds.height()), null); +// masked.recycle(); +// } + + @Override + public PaintContext createContext(Rect deviceBounds, AffineTransform xform) { + PaintContext ctx = this.paint.createContext(deviceBounds, xform); + return new SoftPaintContext(ctx); + } + + private class SoftPaintContext implements PaintContext { + private final PaintContext context; + public SoftPaintContext(PaintContext context) { + this.context = context; + } + + @Override + public Bitmap.Config getColorModel() { + return ARGB_COLOR_MODEL; + } + + @Override + public Bitmap getRaster(int x1, int y1, int w, int h) { + int[] pixels = new int[w]; + // getRaster would return a w*h bitmap + Bitmap origin = context.getRaster(x1, y1, w, h); + // mask size should be greater than w*h + int maskWidth = mask.getWidth(); + int maskHeight = mask.getHeight(); + int[] pixelsMask = new int[maskWidth]; + float[] input = null; + Float[] map = null; + + if (transferFunction != null) { + map = new Float[256]; + input = new float[1]; + } + Bitmap output = Bitmap.createBitmap(w, h, getColorModel()); + + // the soft mask has its own bbox + x1 = x1 - (int) bboxDevice.left; + y1 = y1 - (int) bboxDevice.top; + + int pixelInput = 0; + int[] pixelOutput = new int[4]; + for (int y = 0; y < h; y++) { + origin.getPixels(pixels, 0, w, 0, y, w, 1); + mask.getPixels(pixelsMask, 0, maskWidth, 0, y1 + y, maskWidth, 1); + for (int x = 0; x < w; x++) { + pixelInput = pixels[y * w + x]; + + pixelOutput[0] = Color.red(pixelInput); + pixelOutput[1] = Color.green(pixelInput); + pixelOutput[2] = Color.blue(pixelInput); + pixelOutput[3] = Color.alpha(pixelInput); + + // get the alpha value from the gray mask, if within mask bounds + if (x1 + x >= 0 && y1 + y >= 0 && x1 + x < maskWidth && y1 + y < maskHeight) { + int maskColor = pixelsMask[(y1 + y) * maskWidth + x1 + x]; + int g = Color.red(maskColor); + if (transferFunction != null) { + // apply transfer function + try { + if (map[g] != null) { + // was calculated before + pixelOutput[3] = Math.round(pixelOutput[3] * map[g]); + } else { + // calculate and store in map + input[0] = g / 255f; + float f = transferFunction.eval(input)[0]; + map[g] = f; + pixelOutput[3] = Math.round(pixelOutput[3] * f); + } + } catch (IOException ex) { + // ignore exception, treat as outside + pixelOutput[3] = Math.round(pixelOutput[3] * (bc / 255f)); + } + } else { + pixelOutput[3] = Math.round(pixelOutput[3] * (g / 255f)); + } + } else { + pixelOutput[3] = Math.round(pixelOutput[3] * (bc / 255f)); + } + pixels[y * w + x] = Color.argb(pixelOutput[3], pixelOutput[0], pixelOutput[1], pixelOutput[2]); + } + output.setPixels(pixels, 0, w, 0, y, w, 1); + } + + // buffer + origin.recycle(); + return output; + } + + @Override + public void dispose() { + mask.recycle(); + } + } +} diff --git a/library/src/main/java/com/tom_roush/pdfbox/rendering/WrapPaint.java b/library/src/main/java/com/tom_roush/pdfbox/rendering/WrapPaint.java new file mode 100644 index 00000000..c7b6a68b --- /dev/null +++ b/library/src/main/java/com/tom_roush/pdfbox/rendering/WrapPaint.java @@ -0,0 +1,46 @@ +package com.tom_roush.pdfbox.rendering; + +import android.graphics.ColorSpace; +import android.graphics.Rect; + +import com.tom_roush.harmony.awt.geom.AffineTransform; + +public interface WrapPaint { + + /** + * Creates and returns a {@link PaintContext} used to + * generate the color pattern. + * The arguments to this method convey additional information + * about the rendering operation that may be + * used or ignored on various implementations of the {@code Paint} interface. + * A caller must pass non-{@code null} values for all of the arguments + * except for the {@code ColorModel} argument which may be {@code null} to + * indicate that no specific {@code ColorModel} type is preferred. + * Implementations of the {@code Paint} interface are allowed to use or ignore + * any of the arguments as makes sense for their function, and are + * not constrained to use the specified {@code ColorModel} for the returned + * {@code PaintContext}, even if it is not {@code null}. + * Implementations are allowed to throw {@code NullPointerException} for + * any {@code null} argument other than the {@code ColorModel} argument, + * but are not required to do so. + * + * @param deviceBounds the device space bounding box + * of the graphics primitive being rendered. + * Implementations of the {@code Paint} interface + * are allowed to throw {@code NullPointerException} + * for a {@code null deviceBounds}. + * @param xform the {@link AffineTransform} from user + * space into device space. + * Implementations of the {@code Paint} interface + * are allowed to throw {@code NullPointerException} + * for a {@code null xform}. + * @return the {@code PaintContext} for + * generating color patterns. + * @see PaintContext + * @see ColorSpace + * @see Rect + * @see Rect + * @see AffineTransform + */ + public PaintContext createContext(Rect deviceBounds, AffineTransform xform); +} diff --git a/library/src/main/java/com/tom_roush/pdfbox/util/GraphicsUtil.java b/library/src/main/java/com/tom_roush/pdfbox/util/GraphicsUtil.java new file mode 100644 index 00000000..5ee7eb52 --- /dev/null +++ b/library/src/main/java/com/tom_roush/pdfbox/util/GraphicsUtil.java @@ -0,0 +1,29 @@ +package com.tom_roush.pdfbox.util; + +import android.graphics.Path; +import android.graphics.PointF; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Region; + +public class GraphicsUtil { + public static void addPoint2Rect(RectF rectF, PointF pointF){ + float newx = pointF.x; + float newy = pointF.y; + float x1 = Math.min(rectF.left, newx); + float x2 = Math.max(rectF.right, newx); + float y1 = Math.min(rectF.top, newy); + float y2 = Math.max(rectF.bottom, newy); + rectF.set(x1, y1, x2, y2); + } + + public static Region getPathRegion(Path path){ + RectF bounds = new RectF(); + path.computeBounds(bounds, true); + Region outRegion = new Region(); + Rect boundsRounded = new Rect(); + bounds.round(boundsRounded); + outRegion.setPath(path, new Region(boundsRounded)); + return outRegion; + } +}