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);
}
}