Dear java2d-members,

I am looking in depth into java2d pipelines (composite, mask fill) to
understand if the alpha blending is performed correctly (gamma correction,
color mixing).


FYI, I read a lot of articles related to color blending ans its problems
related to gamma correction & color spaces.

I think that java compositing is gamma corrected (sRGB) but suffers bad
color mixing (luminance and saturation issue).

Look at this article for correct color mixing:
http://www.stuartdenman.com/improved-color-blending/
Correct gamma correction:
http://www.chez-jim.net/agg/gamma-correct-rendering-part-3

I figured out that:
1- gamma correction seems performed correctly by the java2d pipeline
(AlphaComposite.srcOver) ie sRGB <=> linear RGB (blending)

Could someone confirm that gamma correction (sRGB <=> Linear RGB) is used
for normal color blending ?


2- Alpha mask filling (maskFill / maskBlit operators) is used for
antialiasing and several implementations are implemented in C (software,
opengl or xrender variants) but it seems incorrect:

For alpha = 50% (byte=128) => a black line over a white background gives
middle gray (byte=128 and not byte=192): it uses linear on sRGB values
instead of linear RGB values => gamma correction should be fixed !

Maybe text rendering should be improved too ?


3- Colors are blended / mixed in RGB color space => yellow + blue = gray
issue !
    I could try implementing RGB<=>CIE-Lch color mixing ... but it requires
a lot of computations (sRGB <=> Linear RGB <=> CIE-XYZ <=> CIE-LAB <=>
CIE-Lch); so I should take care to use proper approximations or cache
results (LRU color cache).



To illustrate the alpha mask filling problem (2), I hacked the
sun.java2d.GeneralCompositePipe to perform my own mask Fill in java using a
custom Composite (BlendComposite).

In my test, the image is a default RGBA buffered image (sRGB) so I can
easily handle R,G,B & alpha values.

GeneralCompositePipe changes:


*        if (sg.composite instanceof BlendComposite) {            // define
mask alpha into dstOut:*

*1/ Copy the alpha coverage mask (from antialiasing renderer) into dstOut
raster *










*           // INT_RGBA only            final int[] dstPixels = new
int[w];                          for (int j = 0; j < h; j++)
{                for (int i = 0; i < w; i++) {
dstPixels[i] = atile[ j * tilesize + (i + offset)] << 24;
}                dstOut.setDataElements(0, j, w, 1,
dstPixels);              }    *
*2/ Delegate alpha color blending to Java code (my **BlendComposite impl)
      *
*             compCtxt.compose(srcRaster, dstIn, dstOut);*
        }
...
            if (dstRaster instanceof WritableRaster

*                    && ((atile == null) || sg.composite instanceof
BlendComposite)) {*
*3/ As mask fill was done (by *
*the BlendComposite), just copy raster pixels into image *

*                ((WritableRaster) dstRaster).setDataElements(x, y,
dstOut);*            }

BlendComposite:

    private final static BlendComposite.GammaLUT gamma_LUT = new
BlendComposite.GammaLUT(2.2);

        private final static int MAX_COLORS = 256;
        final int[] dir = new int[MAX_COLORS]; // pow(0..1,     2.2)
        final int[] inv = new int[MAX_COLORS]; // pow(0..1, 1./2.2)

It contains the gamma correction (quick & dirty LUT 8bits) that could be
improved to 12 or 16bits ...
That code already exists in DirectColorModel (tosRGB8LUT, fromsRGB8LUT8 =
algorithm for linear RGB to nonlinear sRGB conversion)


Fixing the Alpha mask blending:
           for (int y = 0; y < height; y++) {
                src.getDataElements(0, y, width, 1, srcPixels);
// shape color as pixels
                dstIn.getDataElements(0, y, width, 1, dstPixels);       //
background color as pixels
                dstOut.getDataElements(0, y, width, 1, maskPixels); // get
alpha mask values

                for (int x = 0; x < width; x++) {
                    // pixels are stored as INT_ARGB
                    // our arrays are [R, G, B, A]
                    pixel = maskPixels[x];
                    alpha = (pixel >> 24) & 0xFF;

                    if (alpha == 255) {
                        dstPixels[x] = srcPixels[x]; // opacity = 1 =>
result = shape color
                    } else if (alpha != 0) { // opacity = 0 => result =
background color
//                        System.out.println("alpha = " + alpha);

                        // blend
                        pixel = srcPixels[x];
* // Convert sRGB to linear RGB (gamma correction):*
                        srcPixel[0] = gamma_dir[(pixel >> 16) & 0xFF];
                        srcPixel[1] = gamma_dir[(pixel >> 8) & 0xFF];
                        srcPixel[2] = gamma_dir[(pixel) & 0xFF];
                        srcPixel[3] = (pixel >> 24) & 0xFF;

                        pixel = dstPixels[x];
* // Convert sRGB to linear RGB (gamma correction):*
                        dstPixel[0] = gamma_dir[(pixel >> 16) & 0xFF];
                        dstPixel[1] = gamma_dir[(pixel >> 8) & 0xFF];
                        dstPixel[2] = gamma_dir[(pixel) & 0xFF];
                        dstPixel[3] = (pixel >> 24) & 0xFF;

*// Blend linear RGB & alpha values :*
                        blender.blend(srcPixel, dstPixel, alpha, result);

                        // mixes the result with the opacity
*// Convert linear RGB to sRGB (inverse gamma correction):*
                        dstPixels[x] = (/*result[3] & */0xFF) << 24   //
discard alpha (RGBA blending to be fixed asap)
                                | gamma_inv[result[0] & 0xFF] << 16
                                | gamma_inv[result[1] & 0xFF] << 8
                                | gamma_inv[result[2] & 0xFF];
                    }
                }
* // Copy pixels into raster:*
                dstOut.setDataElements(0, y, width, 1, dstPixels);
            }

My very simple alpha combination uses only the alpha coverage values (not
alpha from src & dst pixel) :
                      public void blend(final int[] src, final int[] dst,
final int alpha, final int[] result) {
                            final float src_alpha = alpha / 255f;
                            final float comp_src_alpha = 1f - src_alpha;
                            // src & dst are gamma corrected

                            result[0] = Math.max(0, Math.min(255, (int)
(src[0] * src_alpha + dst[0] * comp_src_alpha)));
                            result[1] = Math.max(0, Math.min(255, (int)
(src[1] * src_alpha + dst[1] * comp_src_alpha)));
                            result[2] = Math.max(0, Math.min(255, (int)
(src[2] * src_alpha + dst[2] * comp_src_alpha)));
                            result[3] = 255; /* Math.max(0, Math.min(255,
(int) (255f * (src_alpha + comp_src_alpha)))) */
                        }

It could be optimized to use integer maths (not float) later... and perform
other color corrections (CIE-lch interpolation ...) later !


To illustrate changes, look at the LineTests outputs: "ropiness" effect has
disappeared (as expeted) and the antialiased lines looks better !!

Gamma corrected mask fill:
http://apps.jmmc.fr/~bourgesl/share/MaskFill/LinesTest-gamma-corrected-maskFill.png
Original mask fill:
http://apps.jmmc.fr/~bourgesl/share/MaskFill/LinesTest-original-maskFill.png

These images were produced using the marlin-renderer (pisces fork) with
Open JDK 8.

PS: enable/disable the useCustomComposite flag in the paint() method to
enable / disable the fix !
Of course, the modified GeneralCompositePipe & BlendComposite class must be
packaged into a patch.jar and the jvm must be started with
-Xbootclasspath/p:<path>/patch.jar


Looking forward your comments,
Laurent Bourgès
/*
 * Copyright (c) 1997, 2002, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package sun.java2d.pipe;

import java.awt.AlphaComposite;
import java.awt.CompositeContext;
import java.awt.PaintContext;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.RenderingHints;
import java.awt.image.ColorModel;
import java.awt.image.BufferedImage;
import java.awt.image.Raster;
import java.awt.image.WritableRaster;
import sun.awt.image.BufImgSurfaceData;
import sun.java2d.SunGraphics2D;
import sun.java2d.SurfaceData;
import sun.java2d.loops.Blit;
import sun.java2d.loops.MaskBlit;
import sun.java2d.loops.CompositeType;

public class GeneralCompositePipe implements CompositePipe {
    class TileContext {
        SunGraphics2D sunG2D;
        PaintContext paintCtxt;
        CompositeContext compCtxt;
        ColorModel compModel;
        Object pipeState;

        public TileContext(SunGraphics2D sg, PaintContext pCtx,
                           CompositeContext cCtx, ColorModel cModel) {
            sunG2D = sg;
            paintCtxt = pCtx;
            compCtxt = cCtx;
            compModel = cModel;
        }
    }

    public Object startSequence(SunGraphics2D sg, Shape s, Rectangle devR,
                                int[] abox) {
        RenderingHints hints = sg.getRenderingHints();
        ColorModel model = sg.getDeviceColorModel();
        PaintContext paintContext =
            sg.paint.createContext(model, devR, s.getBounds2D(),
                                   sg.cloneTransform(),
                                   hints);
        CompositeContext compositeContext =
            sg.composite.createContext(paintContext.getColorModel(), model,
                                       hints);
        return new TileContext(sg, paintContext, compositeContext, model);
    }

    public boolean needTile(Object ctx, int x, int y, int w, int h) {
        return true;
    }

    /**
    * GeneralCompositePipe.renderPathTile works with custom composite operator
    * provided by an application
    */
    public void renderPathTile(Object ctx,
                               byte[] atile, int offset, int tilesize,
                               int x, int y, int w, int h) {
        TileContext context = (TileContext) ctx;
        PaintContext paintCtxt = context.paintCtxt;
        CompositeContext compCtxt = context.compCtxt;
        SunGraphics2D sg = context.sunG2D;

        Raster srcRaster = paintCtxt.getRaster(x, y, w, h);

        Raster dstRaster;
        Raster dstIn;
        WritableRaster dstOut;

        SurfaceData sd = sg.getSurfaceData();
        dstRaster = sd.getRaster(x, y, w, h);
        if (dstRaster instanceof WritableRaster && atile == null) {
            dstOut = (WritableRaster) dstRaster;
            dstOut = dstOut.createWritableChild(x, y, w, h, 0, 0, null);
            dstIn = dstOut;
        } else {
            dstIn = dstRaster.createChild(x, y, w, h, 0, 0, null);
            dstOut = dstIn.createCompatibleWritableRaster();
        }

        if (sg.composite instanceof BlendComposite) {
            // define mask alpha into dstOut:
            
            // INT_RGBA only
            final int[] dstPixels = new int[w];
            
              for (int j = 0; j < h; j++) {
                for (int i = 0; i < w; i++) {
                    dstPixels[i] = atile[ j * tilesize + (i + offset)] << 24;
                }
                dstOut.setDataElements(0, j, w, 1, dstPixels);
              }
            
             compCtxt.compose(srcRaster, dstIn, dstOut);
        } else {
            compCtxt.compose(srcRaster, dstIn, dstOut);
        }

        if (dstRaster != dstOut && dstOut.getParent() != dstRaster) {
            if (dstRaster instanceof WritableRaster 
                    && ((atile == null) || sg.composite instanceof BlendComposite)) {
                ((WritableRaster) dstRaster).setDataElements(x, y, dstOut);
            } else {
                ColorModel cm = sg.getDeviceColorModel();
                BufferedImage resImg =
                    new BufferedImage(cm, dstOut,
                                      cm.isAlphaPremultiplied(),
                                      null);
                SurfaceData resData = BufImgSurfaceData.createData(resImg);
                if (atile == null) {
                    Blit blit = Blit.getFromCache(resData.getSurfaceType(),
                                                  CompositeType.SrcNoEa,
                                                  sd.getSurfaceType());
                    blit.Blit(resData, sd, AlphaComposite.Src, null,
                              0, 0, x, y, w, h);
                } else {
                    MaskBlit blit = MaskBlit.getFromCache(resData.getSurfaceType(),
                                                          CompositeType.SrcNoEa,
                                                          sd.getSurfaceType());
                    blit.MaskBlit(resData, sd, AlphaComposite.Src, null,
                                  0, 0, x, y, w, h,
                                  atile, offset, tilesize);
                }
            }
        }
    }

    public void skipTile(Object ctx, int x, int y) {
        return;
    }

    public void endSequence(Object ctx) {
        TileContext context = (TileContext) ctx;
        if (context.paintCtxt != null) {
            context.paintCtxt.dispose();
        }
        if (context.compCtxt != null) {
            context.compCtxt.dispose();
        }
    }

}
/*
 * Copyright (c) 2006 Romain Guy <[email protected]>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. The name of the author may not be used to endorse or promote products
 *    derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package sun.java2d.pipe;

import java.awt.Composite;
import java.awt.CompositeContext;
import java.awt.RenderingHints;
import java.awt.image.ColorModel;
import java.awt.image.DataBuffer;
import java.awt.image.Raster;
import java.awt.image.WritableRaster;

public final class BlendComposite implements Composite {

    private final static BlendComposite.GammaLUT gamma_LUT = new BlendComposite.GammaLUT(2.2);

    public static class GammaLUT {

        private final static int MAX_COLORS = 256;
        final int[] dir = new int[MAX_COLORS];
        final int[] inv = new int[MAX_COLORS];

        GammaLUT(final double gamma) {
            final double max = (double) (MAX_COLORS - 1);
            final double invGamma = 1.0 / gamma;

            for (int i = 0; i < MAX_COLORS; i++) {
                dir[i] = (int) (max * Math.pow(i / max, gamma));
                inv[i] = (int) (max * Math.pow(i / max, invGamma));
//                System.out.println("dir[" + i + "] = " + dir[i]);
//                System.out.println("inv[" + i + "] = " + inv[i]);
            }
        }
    }

    public enum BlendingMode {

        SRC_OVER
    }
    public static final BlendComposite SrcOver = new BlendComposite(BlendComposite.BlendingMode.SRC_OVER);
    private BlendComposite.BlendingMode mode;

    private BlendComposite(BlendComposite.BlendingMode mode) {
        this.mode = mode;
    }

    public static BlendComposite getInstance(BlendComposite.BlendingMode mode) {
        return new BlendComposite(mode);
    }

    public BlendComposite.BlendingMode getMode() {
        return mode;
    }

    @Override
    public int hashCode() {
        return mode.ordinal();
    }

    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof BlendComposite)) {
            return false;
        }

        BlendComposite bc = (BlendComposite) obj;

        return (mode == bc.mode);
    }

    public CompositeContext createContext(ColorModel srcColorModel,
            ColorModel dstColorModel,
            RenderingHints hints) {
        return new BlendComposite.BlendingContext(this);
    }

    private static final class BlendingContext implements CompositeContext {

        private final BlendComposite.Blender blender;
        private final BlendComposite composite;

        private BlendingContext(BlendComposite composite) {
            this.composite = composite;
            this.blender = BlendComposite.Blender.getBlenderFor(composite);
        }

        public void dispose() {
        }

        public synchronized void compose(Raster src, Raster dstIn, WritableRaster dstOut) {
            if (src.getSampleModel().getDataType() != DataBuffer.TYPE_INT
                    || dstIn.getSampleModel().getDataType() != DataBuffer.TYPE_INT
                    || dstOut.getSampleModel().getDataType() != DataBuffer.TYPE_INT) {
                throw new IllegalStateException(
                        "Source and destination must store pixels as INT.");
            }
            /*
            System.out.println("src = " + src.getBounds());
            System.out.println("dstIn = " + dstIn.getBounds());
            System.out.println("dstOut = " + dstOut.getBounds());
*/
            final int width = Math.min(src.getWidth(), dstIn.getWidth());
            final int height = Math.min(src.getHeight(), dstIn.getHeight());

            final int[] gamma_dir = gamma_LUT.dir;
            final int[] gamma_inv = gamma_LUT.inv;

            final int[] srcPixel = new int[4];
            final int[] dstPixel = new int[4];
            final int[] result = new int[4];
            int alpha;
            
            final int[] srcPixels = new int[width];
            final int[] dstPixels = new int[width];
            final int[] maskPixels = new int[width];

            int pixel;

            for (int y = 0; y < height; y++) {
                src.getDataElements(0, y, width, 1, srcPixels);
                dstIn.getDataElements(0, y, width, 1, dstPixels);
                dstOut.getDataElements(0, y, width, 1, maskPixels);

                for (int x = 0; x < width; x++) {
                    // pixels are stored as INT_ARGB
                    // our arrays are [R, G, B, A]
                    pixel = maskPixels[x];
                    alpha = (pixel >> 24) & 0xFF;

                    if (alpha == 255) {
                        dstPixels[x] = srcPixels[x];
                    } else if (alpha != 0) {
//                        System.out.println("alpha = " + alpha);
                        
                        // blend
                        pixel = srcPixels[x];
                        srcPixel[0] = gamma_dir[(pixel >> 16) & 0xFF];
                        srcPixel[1] = gamma_dir[(pixel >> 8) & 0xFF];
                        srcPixel[2] = gamma_dir[(pixel) & 0xFF];
                        srcPixel[3] = (pixel >> 24) & 0xFF;

                        pixel = dstPixels[x];
                        dstPixel[0] = gamma_dir[(pixel >> 16) & 0xFF];
                        dstPixel[1] = gamma_dir[(pixel >> 8) & 0xFF];
                        dstPixel[2] = gamma_dir[(pixel) & 0xFF];
                        dstPixel[3] = (pixel >> 24) & 0xFF;

                        // recycle int[] instances:
                        blender.blend(srcPixel, dstPixel, alpha, result);

                        // mixes the result with the opacity
                        dstPixels[x] = (/*result[3] & */0xFF) << 24
                                | gamma_inv[result[0] & 0xFF] << 16
                                | gamma_inv[result[1] & 0xFF] << 8
                                | gamma_inv[result[2] & 0xFF];
                    }
                }
                dstOut.setDataElements(0, y, width, 1, dstPixels);
            }
        }
    }

    /**
     * linear RGB --> sRGB Use the inverse gamma curve
     */
    public static float toRGB(float in) {
        float n = in;
        if (n < 0) {
            n = 0f;
        }
        if (n > 1) {
            n = 1f;
        }
        if (n <= 0.00304f) {
            return n * 12.92f;
        } else {
            return 1.055f * ((float) Math.exp((1 / 2.4) * Math.log(n))) - 0.055f;
        }
    }

    /**
     * sRGB --> linear RGB Use the gamma curve (gamma=2.4 in sRGB)
     */
    public static float fromRGB(float in) {

        // Convert non-linear RGB coordinates to linear ones,
        //  numbers from the w3 spec.

        float n = in;
        if (n < 0) {
            n = 0f;
        }
        if (n > 1) {
            n = 1f;
        }
        if (n <= 0.03928f) {
            return n / 12.92f;
        } else {
            return (float) (Math.exp(2.4 * Math.log((n + 0.055) / 1.055)));
        }
    }

    private static abstract class Blender {

        public abstract void blend(int[] src, int[] dst, int alpha, int[] result);

        public static BlendComposite.Blender getBlenderFor(BlendComposite composite) {
            switch (composite.getMode()) {
                case SRC_OVER:
                    return new BlendComposite.Blender() {
                        @Override
                        public void blend(final int[] src, final int[] dst, final int alpha, final int[] result) {
                            final float src_alpha = alpha / 255f;
                            final float comp_src_alpha = 1f - src_alpha;
                            // src & dst are gamma corrected

                            result[0] = Math.max(0, Math.min(255, (int) (src[0] * src_alpha + dst[0] * comp_src_alpha)));
                            result[1] = Math.max(0, Math.min(255, (int) (src[1] * src_alpha + dst[1] * comp_src_alpha)));
                            result[2] = Math.max(0, Math.min(255, (int) (src[2] * src_alpha + dst[2] * comp_src_alpha)));
                            result[3] = 255; /* Math.max(0, Math.min(255, (int) (255f * (src_alpha + comp_src_alpha)))) */
                        }
                    };
                default:
            }
            throw new IllegalArgumentException("Blender not implement for " + composite.getMode().name());
        }
    }
}
/*
 * Copyright (c) 2014, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */
package test;

import sun.java2d.pipe.BlendComposite;
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Composite;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.color.ColorSpace;
import java.awt.geom.Path2D;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.DataBuffer;
import java.awt.image.DirectColorModel;
import java.awt.image.WritableRaster;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
import org.marlin.pisces.PiscesRenderingEngine;

/**
 * Simple Line rendering test using GeneralPath to enable Pisces / marlin / ductus renderers
 */
public class LineTests {

    private final static String FILE_NAME = "LinesTest-gamma-norm-subpix_lg_";
    private final static boolean useColor = true;
    private final static Color COL_1 = (useColor) ? Color.blue : Color.white;
    private final static Color COL_2 = (useColor) ? Color.yellow : Color.black;

    public static void main(String[] args) {
        final double lineStroke = 2.0;
        final int size = 600;
        final int width = size + 100;
        final int height = size;

        System.out.println("LineTests: size = " + width + " x " + height);

        final boolean useLinearRGB = false;

        final BufferedImage image;

        if (useLinearRGB) {
            final ColorModel cm = new DirectColorModel(
                    ColorSpace.getInstance(ColorSpace.CS_LINEAR_RGB),
                    32,
                    0x00ff0000, // Red
                    0x0000ff00, // Green
                    0x000000ff, // Blue
                    0xff000000, // Alpha
                    false,
                    DataBuffer.TYPE_INT);

            final WritableRaster raster = cm.createCompatibleWritableRaster(width, height);

            image = new BufferedImage(cm, raster, false, null);
        } else {
            image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
        }

        final Graphics2D g2d = (Graphics2D) image.getGraphics();
        g2d.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        g2d.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
        g2d.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_ENABLE);
        g2d.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
        g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
        g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_NORMALIZE);

        g2d.setClip(0, 0, width, height);

        g2d.setBackground(COL_1);
        g2d.clearRect(0, 0, width, height);

        final long start = System.nanoTime();

        paint(g2d, width, height);

        final long time = System.nanoTime() - start;

        System.out.println("paint: duration= " + (1e-6 * time) + " ms.");

        try {
            final File file = new File(FILE_NAME + PiscesRenderingEngine.getSubPixel_Log2_X()
                    + "x" + PiscesRenderingEngine.getSubPixel_Log2_Y() + ".png");

            System.out.println("Writing file: " + file.getAbsolutePath());;
            ImageIO.write(image, "PNG", file);
        } catch (IOException ex) {
            ex.printStackTrace();
        } finally {
            g2d.dispose();
        }
    }

    private static void paint(final Graphics2D g2d, final double width, final double height) {

        final double size = Math.min(width, height);

        final boolean useCustomComposite = true;

        Composite c = (useCustomComposite)
                ? BlendComposite.getInstance(BlendComposite.BlendingMode.SRC_OVER)
                : AlphaComposite.SrcOver;

        g2d.setComposite(c);

        double thinStroke = 1.5;
        double lineStroke = 2.5;

        final Path2D.Float path = new Path2D.Float();

        for (double angle = 1d / 5d; angle <= 90d; angle += 1d) {
            double angRad = Math.toRadians(angle);

            double cos = Math.cos(angRad);
            double sin = Math.sin(angRad);

            // same algo as agg:
            drawLine(path, 5d * cos, 5d * sin, size * cos, size * sin, lineStroke);

            g2d.setColor(COL_2);
            g2d.fill(path);
            /*
             drawLine(path, 5d * cos, 5d * sin, size * cos, size * sin, thinStroke);

             g2d.setColor(Color.GREEN);
             g2d.fill(path);
             */
            if (false) {
                break;
            }
        }

        final double rectW = Math.abs(width - height);
        if (rectW > 0.0) {
            final int w = (int) (rectW / 2.);
            final double step = 0.01;
            final double yStep = step * height;
            double alpha = 0.0;

            // BlendingMode.SRC_OVER

            for (double y = 0; y < height; y += yStep, alpha += step) {
                g2d.setColor(new Color(COL_2.getRed(), COL_2.getGreen(), COL_2.getBlue(), (int) (255 * alpha)));
                g2d.fillRect((int) height, (int) y, w, (int) yStep);
            }

            c = AlphaComposite.SrcOver;
            g2d.setComposite(c);
            alpha = 0.0;

            for (double y = 0; y < height; y += yStep, alpha += step) {
                g2d.setColor(new Color(COL_2.getRed(), COL_2.getGreen(), COL_2.getBlue(), (int) (255 * alpha)));
                g2d.fillRect((int) height + w, (int) y, w, (int) yStep);
            }
        }
    }

    private static void drawLine(final Path2D.Float path,
            double x1, double y1,
            double x2, double y2,
            double width) {

        double dx = x2 - x1;
        double dy = y2 - y1;
        double d = Math.sqrt(dx * dx + dy * dy);

        dx = width * (y2 - y1) / d;
        dy = width * (x2 - x1) / d;

        path.reset();

        path.moveTo(x1 - dx, y1 + dy);
        path.lineTo(x2 - dx, y2 + dy);
        path.lineTo(x2 + dx, y2 - dy);
        path.lineTo(x1 + dx, y1 - dy);
    }
}

Reply via email to