# TilingPaint.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.Graphics2D;
    import java.awt.Paint;
    import java.awt.PaintContext;
    import java.awt.Rectangle;
    import java.awt.RenderingHints;
    import java.awt.TexturePaint;
    import java.awt.Transparency;
    import java.awt.geom.AffineTransform;
    import java.awt.geom.Rectangle2D;
    import java.awt.image.BufferedImage;
    import java.awt.image.ColorModel;
    import java.io.IOException;
    import java.math.BigDecimal;
    import java.math.RoundingMode;
    import org.apache.commons.logging.Log;
    import org.apache.commons.logging.LogFactory;
    import org.apache.pdfbox.pdmodel.common.PDRectangle;
    import org.apache.pdfbox.pdmodel.graphics.color.PDColor;
    import org.apache.pdfbox.pdmodel.graphics.color.PDColorSpace;
    import org.apache.pdfbox.pdmodel.graphics.pattern.PDTilingPattern;
    import org.apache.pdfbox.util.Matrix;

    /**
     * AWT Paint for a tiling pattern, which consists of a small repeating graphical figure.
     *
     * @author John Hewson
     */
    class TilingPaint implements Paint
    {
        private static final Log LOG = LogFactory.getLog(TilingPaint.class);
        private final Paint paint;
        private final Matrix patternMatrix;
        private static final int MAXEDGE;
        private static final String DEFAULTMAXEDGE = "3000";

        static
        {
            String s = System.getProperty("pdfbox.rendering.tilingpaint.maxedge", DEFAULTMAXEDGE);
            int val;
            try
            {
                val = Integer.parseInt(s);
            }
            catch (NumberFormatException ex)
            {
                LOG.error("Default will be used", ex);
                val = Integer.parseInt(DEFAULTMAXEDGE);
            }
            MAXEDGE = val;
        }

        /**
         * Creates a new colored tiling Paint, i.e. one that has its own colors.
         *
         * @param drawer renderer to render the page
         * @param pattern tiling pattern dictionary
         * @param xform device scale transform
         *
         * @throws java.io.IOException if something goes wrong while drawing the pattern
         */
        TilingPaint(PageDrawer drawer, PDTilingPattern pattern, AffineTransform xform)
                throws IOException
        {
            this(drawer, pattern, null, null, xform);
        }

        /**
         * Creates a new tiling Paint. The parameters color and colorSpace must be null for a colored
         * tiling Paint (because it has its own colors), and non null for an uncolored tiling Paint.
         *
         * @param drawer renderer to render the page
         * @param pattern tiling pattern dictionary
         * @param colorSpace color space for this tiling
         * @param color color for this tiling
         * @param xform device scale transform
         *
         * @throws java.io.IOException if something goes wrong while drawing the pattern
         */
        TilingPaint(PageDrawer drawer, PDTilingPattern pattern, PDColorSpace colorSpace,
                           PDColor color, AffineTransform xform) throws IOException
        {
            // pattern space -> user space
            patternMatrix = Matrix.concatenate(drawer.getInitialMatrix(), pattern.getMatrix());
            Rectangle2D anchorRect = getAnchorRect(pattern);
            paint = new TexturePaint(getImage(drawer, pattern, colorSpace, color, xform, anchorRect), anchorRect);
        }

        /**
         * Not called in TexturePaint subclasses, which is why we wrap TexturePaint.
         */
        @Override
        public PaintContext createContext(ColorModel cm, Rectangle deviceBounds, Rectangle2D userBounds,
                                          AffineTransform xform, RenderingHints hints)
        {
            AffineTransform xformPattern = (AffineTransform)xform.clone();

            // applies the pattern matrix with scaling removed
            AffineTransform patternNoScale = patternMatrix.createAffineTransform();
            patternNoScale.scale(1 / patternMatrix.getScalingFactorX(),
                                 1 / patternMatrix.getScalingFactorY());
            xformPattern.concatenate(patternNoScale);

            return paint.createContext(cm, deviceBounds, userBounds, xformPattern, hints);
        }

        /**
         * Returns the pattern image in parent stream coordinates.
         */
        private BufferedImage getImage(PageDrawer drawer, PDTilingPattern pattern, PDColorSpace colorSpace,
                PDColor color, AffineTransform xform, Rectangle2D anchorRect) throws IOException
        {
            float width = (float) Math.abs(anchorRect.getWidth());
            float height = (float) Math.abs(anchorRect.getHeight());

            // device scale transform (i.e. DPI) (see PDFBOX-1466.pdf)
            Matrix xformMatrix = new Matrix(xform);
            float xScale = Math.abs(xformMatrix.getScalingFactorX());
            float yScale = Math.abs(xformMatrix.getScalingFactorY());
            width *= xScale;
            height *= yScale;

            int rasterWidth = Math.max(1, ceiling(width));
            int rasterHeight = Math.max(1, ceiling(height));

            BufferedImage image = new BufferedImage(rasterWidth, rasterHeight, BufferedImage.TYPE_INT_ARGB);

            Graphics2D graphics = image.createGraphics();

            // flip a -ve YStep around its own axis (see gs-bugzilla694385.pdf)
            if (pattern.getYStep() < 0)
            {
                graphics.translate(0, rasterHeight);
                graphics.scale(1, -1);
            }

            // flip a -ve XStep around its own axis
            if (pattern.getXStep() < 0)
            {
                graphics.translate(rasterWidth, 0);
                graphics.scale(-1, 1);
            }

            // device scale transform (i.e. DPI)
            graphics.scale(xScale, yScale);

            // apply only the scaling from the pattern transform, doing scaling here improves the
            // image quality and prevents large scale-down factors from creating huge tiling cells.
            Matrix newPatternMatrix = Matrix.getScaleInstance(
                    Math.abs(patternMatrix.getScalingFactorX()),
                    Math.abs(patternMatrix.getScalingFactorY()));

            // move origin to (0,0)
            PDRectangle bBox = pattern.getBBox();
            newPatternMatrix.translate(-bBox.getLowerLeftX(), -bBox.getLowerLeftY());

            // render using PageDrawer
            drawer.drawTilingPattern(graphics, pattern, colorSpace, color, newPatternMatrix);
            graphics.dispose();

            return image;
        }

        /**
         * Returns the closest integer which is larger than the given number.
         * Uses BigDecimal to avoid floating point error which would cause gaps in the tiling.
         */
        private static int ceiling(double num)
        {
            BigDecimal decimal = BigDecimal.valueOf(num);
            decimal = decimal.setScale(5, RoundingMode.CEILING); // 5 decimal places of accuracy
            return decimal.intValue();
        }

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

        /**
         * Returns the anchor rectangle, which includes the XStep/YStep and scaling.
         */
        private Rectangle2D getAnchorRect(PDTilingPattern pattern) throws IOException
        {
            PDRectangle bbox = pattern.getBBox();
            if (bbox == null)
            {
                throw new IOException("Pattern /BBox is missing");
            }
            float xStep = pattern.getXStep();
            if (Float.compare(xStep, 0) == 0)
            {
                LOG.warn("/XStep is 0, using pattern /BBox width");
                xStep = bbox.getWidth();
            }

            float yStep = pattern.getYStep();
            if (Float.compare(yStep, 0) == 0)
            {
                LOG.warn("/YStep is 0, using pattern /BBox height");
                yStep = bbox.getHeight();
            }

            float xScale = patternMatrix.getScalingFactorX();
            float yScale = patternMatrix.getScalingFactorY();
            float width = xStep * xScale;
            float height = yStep * yScale;

            if (Math.abs(width * height) > MAXEDGE * MAXEDGE)
            {
                // PDFBOX-3653: prevent huge sizes
                LOG.info("Pattern surface is too large, will be clipped");
                LOG.info("width: " + width + ", height: " + height);
                LOG.info("XStep: " + xStep + ", YStep: " + yStep);
                LOG.info("bbox: " + bbox);
                LOG.info("pattern matrix: " + pattern.getMatrix());
                LOG.info("concatenated matrix: " + patternMatrix);
                width = Math.min(MAXEDGE, Math.abs(width)) * Math.signum(width);
                height = Math.min(MAXEDGE, Math.abs(height)) * Math.signum(height);
                //TODO better solution needed
            }

            // returns the anchor rect with scaling applied
            return new Rectangle2D.Float(bbox.getLowerLeftX() * xScale,
                                         bbox.getLowerLeftY() * yScale,
                                         width, height);
        }
    }