Bartosz Woźniak
Bartosz Woźniak

Reputation: 2135

Creating a gradient in background with PDFBox

How can I create a gradient in PDFBox? Or maybe "can I?".

I don't want to create them and export to jpeg or something else. I need a light document, so this has to be programmed somehow.

Any ideas?

Upvotes: 2

Views: 1136

Answers (2)

Nicolas
Nicolas

Reputation: 7081

Here's a class I made to make the creation of gradients easier. It supports axial gradients with multiple colors. It uses java.awt.Color to specify colors but that can be replaced easily.

public class PDGradient extends PDShadingType2 {

    public PDGradient(List<GradientPart> parts) {
        super(new COSDictionary());

        // PDF 1.7 - 8.7.4.5.3 Type 2 (Axial) Shadings
        setColorSpace(PDDeviceRGB.INSTANCE);
        setShadingType(PDShadingType2.SHADING_TYPE2);
        setFunction(createGradientFunction(parts));
    }

    private static PDFunction createGradientFunction(List<GradientPart> parts) {
        if (parts.size() < 2) {
            throw new IllegalArgumentException("Gradient must have at least 2 colors.");
        }

        GradientPart first = parts.get(0);
        GradientPart last = parts.get(parts.size() - 1);
        if (first.ratio != 0f) {
            throw new IllegalArgumentException("Gradient first color ratio must be 0.");
        } else if (last.ratio != 1f) {
            throw new IllegalArgumentException("Gradient last color ratio must be 1.");
        }
        if (parts.size() == 2) {
            // Only two colors, use exponential function.
            return createColorFunction(first.color, last.color);
        }

        // Multiple colors, use stitching function to combine exponential functions
        // PDF 1.7 - 7.10.4 Type 3 (Stitching) Functions
        COSDictionary dict = new COSDictionary();
        COSArray functions = new COSArray();
        COSArray bounds = new COSArray();
        COSArray encode = new COSArray();
        GradientPart lastPart = first;
        for (int i = 1; i < parts.size(); i++) {
            GradientPart part = parts.get(i);

            // Add exponential function for interpolating between these two colors.
            functions.add(createColorFunction(lastPart.color, part.color));

            // Specify function bounds, except for first and last, which are specified by domain.
            if (i != parts.size() - 1) {
                bounds.add(new COSFloat(part.ratio));
            }

            // Used to interpolate stitching function subdomain (eg: [0.2 0.5] 
            // to the exponential function domain, which is always [0.0 1.0].
            encode.add(COSInteger.ZERO);
            encode.add(COSInteger.ONE);

            lastPart = part;
        }

        dict.setInt(COSName.FUNCTION_TYPE, 3);
        dict.setItem(COSName.DOMAIN, new PDRange());  // [0.0 1.0]
        dict.setItem(COSName.FUNCTIONS, functions);
        dict.setItem(COSName.BOUNDS, bounds);
        dict.setItem(COSName.ENCODE, encode);

        return new PDFunctionType3(dict);
    }

    private static PDFunction createColorFunction(Color start, Color end) {
        // PDF 1.7 - 7.10.3 Type 2 (Exponential Interpolation) Functions
        COSDictionary dict = new COSDictionary();
        dict.setInt(COSName.FUNCTION_TYPE, 2);
        dict.setItem(COSName.DOMAIN, new PDRange());  // [0.0 1.0]
        dict.setItem(COSName.C0, createColorCOSArray(start));
        dict.setItem(COSName.C1, createColorCOSArray(end));
        dict.setInt(COSName.N, 1);  // Linear interpolation
        return new PDFunctionType2(dict);
    }

    private static COSArray createColorCOSArray(Color color) {
        // Create a COSArray for a color. 
        // java.awt.Color uses 0-255 values while PDF uses 0-1.
        COSArray a = new COSArray();
        a.add(new COSFloat(color.getRed() / 255f));
        a.add(new COSFloat(color.getGreen() / 255f));
        a.add(new COSFloat(color.getBlue() / 255f));
        return a;
    }

    /**
     * Specifies a color and its position in a {@link PDGradient}.
     */
    public static class GradientPart {

        public final Color color;
        public final float ratio;

        public GradientPart(Color color, float ratio) {
            this.color = color;
            this.ratio = ratio;
        }
    }
}

Example usage:

List<GradientPart> parts = new ArrayList<>();
parts.add(new GradientPart(Color.RED, 0.0f));
parts.add(new GradientPart(Color.YELLOW, 0.5f));
parts.add(new GradientPart(Color.GREEN, 1.0f));
PDGradient gradient = new PDGradient(parts);
gradient.setCoords(...);
pdfStream.shadingFill(gradient)

This works essentially the same as the other answer for two colors gradients, using an exponential function (type 2) to linearly interpolate between two colors. If there are more colors, a stitching (type 3) function is used to combine multiple exponential functions with different subdomains.

Upvotes: 1

Bartosz Woźniak
Bartosz Woźniak

Reputation: 2135

After a lot of research, I finally created a small "creator of my own gradient"! It looks like this:

COSDictionary fdict = new COSDictionary();

fdict.setInt(COSName.FUNCTION_TYPE, 2); // still not understaning that...

COSArray domain = new COSArray();
domain.add(COSInteger.get(0));
domain.add(COSInteger.get(1));

COSArray c0 = new COSArray();
c0.add(COSFloat.get("0.64176"));
c0.add(COSFloat.get("0.72588"));
c0.add(COSFloat.get("0.78078"));

COSArray c1 = new COSArray();
c1.add(COSFloat.get("0.57176"));
c1.add(COSFloat.get("0.62588"));
c1.add(COSFloat.get("0.70078"));

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(COSInteger.get(0));
coords1.add(COSInteger.get(0));
coords1.add(COSInteger.get(850)); // size of my page
coords1.add(COSInteger.get(600));

axialShading.setCoords(coords1); // so this sets the bounds of my gradient
axialShading.setFunction(func); // and this determines all the curves etc?

CStr.shadingFill(axialShading); // where CStr is a ContentStream for my PDDocument

I will leave this for others. Leave your opinions and be free to show me some clever ideas to improve this code :)

Upvotes: 3

Related Questions