pdfbox-trunk/pdfbox/src/main/java/org/apache/pdfbox/rendering/SoftMask.java
/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.pdfbox.rendering;

import java.awt.Color;
import java.awt.Paint;
import java.awt.PaintContext;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.Raster;
import java.awt.image.WritableRaster;
import java.io.IOException;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.apache.pdfbox.pdmodel.common.function.PDFunction;
import org.apache.pdfbox.pdmodel.common.function.PDFunctionTypeIdentity;
import org.apache.pdfbox.pdmodel.graphics.color.PDColor;

/**
 * A Paint which applies a soft mask to an underlying Paint.
 * 
 * @author Petr Slaby
 * @author John Hewson
 * @author Matthias Bläsing
 * @author Tilman Hausherr
 */
class SoftMask implements Paint
{

    private static final Log LOG = LogFactory.getLog(SoftMask.class);

    private static final ColorModel ARGB_COLOR_MODEL =
            new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB).getColorModel();

    private final Paint paint;
    private final BufferedImage mask;
    private final Rectangle2D 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(Paint paint, BufferedImage mask, Rectangle2D 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
            {
                Color color = new Color(backdropColor.toRGB());
                // http://stackoverflow.com/a/25463098/535646
                bc = (299 * color.getRed() + 587 * color.getGreen() + 114 * color.getBlue()) / 1000;
            }
            catch (IOException ex)
            {
                // keep default
                LOG.debug("Couldn't convert backdropColor to RGB - keeping default", ex);
            }
        }
    }

    @Override
    public PaintContext createContext(ColorModel cm, Rectangle deviceBounds,
                                      Rectangle2D userBounds, AffineTransform xform,
                                      RenderingHints hints)
    {
        PaintContext ctx = paint.createContext(cm, deviceBounds, userBounds, xform, hints);
        return new SoftPaintContext(ctx);
    }

    @Override
    public int getTransparency()
    {
        return TRANSLUCENT;
    }

    private class SoftPaintContext implements PaintContext
    {
        private final PaintContext context;

        SoftPaintContext(PaintContext context)
        {
            this.context = context;
        }

        @Override
        public ColorModel getColorModel()
        {
            return ARGB_COLOR_MODEL;
        }

        @Override
        public Raster getRaster(int x1, int y1, int w, int h)
        {
            Raster raster = context.getRaster(x1, y1, w, h);
            ColorModel rasterCM = context.getColorModel();
            float[] input = null;
            Float[] map = null;

            if (transferFunction != null)
            {
                map = new Float[256];
                input = new float[1];
            }

            // buffer
            WritableRaster output = getColorModel().createCompatibleWritableRaster(w, h);

            // the soft mask has its own bbox
            x1 = x1 - (int)bboxDevice.getX();
            y1 = y1 - (int)bboxDevice.getY();

            int[] gray = new int[4];
            Object pixelInput = null;
            int[] pixelOutput = new int[4];
            for (int y = 0; y < h; y++)
            {
                for (int x = 0; x < w; x++)
                {
                    pixelInput = raster.getDataElements(x, y, pixelInput);

                    pixelOutput[0] = rasterCM.getRed(pixelInput);
                    pixelOutput[1] = rasterCM.getGreen(pixelInput);
                    pixelOutput[2] = rasterCM.getBlue(pixelInput);
                    pixelOutput[3] = rasterCM.getAlpha(pixelInput);
                    
                    // get the alpha value from the gray mask, if within mask bounds
                    gray[0] = 0;
                    if (x1 + x >= 0 && y1 + y >= 0 && x1 + x < mask.getWidth() && y1 + y < mask.getHeight())
                    {
                        mask.getRaster().getPixel(x1 + x, y1 + y, gray);
                        int g = gray[0];
                        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
                                LOG.debug("Couldn't apply transferFunction - treating as outside", ex);
                                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));
                    }
                    output.setPixel(x, y, pixelOutput);
                }
            }

            return output;
        }

        @Override
        public void dispose()
        {
            context.dispose();
        }
    }
}
