blob: 8d374dc6fd32a199ea0b1725086be6eb8991e84d [file] [log] [blame]
/*
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.batik.bridge;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Paint;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.color.ICC_Profile;
import java.io.IOException;
import org.apache.batik.css.engine.SVGCSSEngine;
import org.apache.batik.css.engine.value.Value;
import org.apache.batik.css.engine.value.svg.ICCColor;
import org.apache.batik.css.engine.value.svg12.CIELabColor;
import org.apache.batik.css.engine.value.svg12.DeviceColor;
import org.apache.batik.css.engine.value.svg12.ICCNamedColor;
import org.apache.batik.gvt.CompositeShapePainter;
import org.apache.batik.gvt.FillShapePainter;
import org.apache.batik.gvt.GraphicsNode;
import org.apache.batik.gvt.Marker;
import org.apache.batik.gvt.MarkerShapePainter;
import org.apache.batik.gvt.ShapeNode;
import org.apache.batik.gvt.ShapePainter;
import org.apache.batik.gvt.StrokeShapePainter;
import org.apache.batik.util.CSSConstants;
import org.apache.batik.util.SVGConstants;
import org.apache.xmlgraphics.java2d.color.CIELabColorSpace;
import org.apache.xmlgraphics.java2d.color.ColorSpaces;
import org.apache.xmlgraphics.java2d.color.ColorWithAlternatives;
import org.apache.xmlgraphics.java2d.color.DeviceCMYKColorSpace;
import org.apache.xmlgraphics.java2d.color.ICCColorSpaceWithIntent;
import org.apache.xmlgraphics.java2d.color.NamedColorSpace;
import org.apache.xmlgraphics.java2d.color.profile.NamedColorProfile;
import org.apache.xmlgraphics.java2d.color.profile.NamedColorProfileParser;
import org.w3c.dom.Element;
import org.w3c.dom.css.CSSPrimitiveValue;
import org.w3c.dom.css.CSSValue;
/**
* A collection of utility methods to deliver <code>java.awt.Paint</code>,
* <code>java.awt.Stroke</code> objects that could be used to paint a
* shape. This class also provides additional methods the deliver SVG
* Paint using the ShapePainter interface.
*
* @author <a href="mailto:tkormann@apache.org">Thierry Kormann</a>
* @version $Id$
*/
public abstract class PaintServer
implements SVGConstants, CSSConstants, ErrorConstants {
/**
* No instance of this class is required.
*/
protected PaintServer() {}
/////////////////////////////////////////////////////////////////////////
// 'marker-start', 'marker-mid', 'marker-end' delegates to the PaintServer
/////////////////////////////////////////////////////////////////////////
/**
* Returns a <code>ShapePainter</code> defined on the specified
* element and for the specified shape node.
*
* @param e the element with the marker CSS properties
* @param node the shape node
* @param ctx the bridge context
*/
public static ShapePainter convertMarkers(Element e,
ShapeNode node,
BridgeContext ctx) {
Value v;
v = CSSUtilities.getComputedStyle(e, SVGCSSEngine.MARKER_START_INDEX);
Marker startMarker = convertMarker(e, v, ctx);
v = CSSUtilities.getComputedStyle(e, SVGCSSEngine.MARKER_MID_INDEX);
Marker midMarker = convertMarker(e, v, ctx);
v = CSSUtilities.getComputedStyle(e, SVGCSSEngine.MARKER_END_INDEX);
Marker endMarker = convertMarker(e, v, ctx);
if (startMarker != null || midMarker != null || endMarker != null) {
MarkerShapePainter p = new MarkerShapePainter(node.getShape());
p.setStartMarker(startMarker);
p.setMiddleMarker(midMarker);
p.setEndMarker(endMarker);
return p;
} else {
return null;
}
}
/////////////////////////////////////////////////////////////////////////
// org.apache.batik.gvt.Marker
/////////////////////////////////////////////////////////////////////////
/**
* Returns a <code>Marker</code> defined on the specified element by
* the specified value, and for the specified shape node.
*
* @param e the painted element
* @param v the CSS value describing the marker to construct
* @param ctx the bridge context
*/
public static Marker convertMarker(Element e,
Value v,
BridgeContext ctx) {
if (v.getPrimitiveType() == CSSPrimitiveValue.CSS_IDENT) {
return null; // 'none'
} else {
String uri = v.getStringValue();
Element markerElement = ctx.getReferencedElement(e, uri);
Bridge bridge = ctx.getBridge(markerElement);
if (bridge == null || !(bridge instanceof MarkerBridge)) {
throw new BridgeException(ctx, e, ERR_CSS_URI_BAD_TARGET,
new Object[] {uri});
}
return ((MarkerBridge)bridge).createMarker(ctx, markerElement, e);
}
}
/////////////////////////////////////////////////////////////////////////
// 'stroke', 'fill' ... converts to ShapePainter
/////////////////////////////////////////////////////////////////////////
/**
* Returns a <code>ShapePainter</code> defined on the specified element and
* for the specified shape node, and using the specified bridge
* context.
*
* @param e the element interested in a shape painter
* @param node the shape node
* @param ctx the bridge context
*/
public static ShapePainter convertFillAndStroke(Element e,
ShapeNode node,
BridgeContext ctx) {
Shape shape = node.getShape();
if (shape == null) return null;
Paint fillPaint = convertFillPaint (e, node, ctx);
FillShapePainter fp = new FillShapePainter(shape);
fp.setPaint(fillPaint);
Stroke stroke = convertStroke (e);
if (stroke == null)
return fp;
Paint strokePaint = convertStrokePaint(e, node, ctx);
StrokeShapePainter sp = new StrokeShapePainter(shape);
sp.setStroke(stroke);
sp.setPaint(strokePaint);
CompositeShapePainter cp = new CompositeShapePainter(shape);
cp.addShapePainter(fp);
cp.addShapePainter(sp);
return cp;
}
public static ShapePainter convertStrokePainter(Element e,
ShapeNode node,
BridgeContext ctx) {
Shape shape = node.getShape();
if (shape == null) return null;
Stroke stroke = convertStroke(e);
if (stroke == null)
return null;
Paint strokePaint = convertStrokePaint(e, node, ctx);
StrokeShapePainter sp = new StrokeShapePainter(shape);
sp.setStroke(stroke);
sp.setPaint(strokePaint);
return sp;
}
/////////////////////////////////////////////////////////////////////////
// java.awt.Paint
/////////////////////////////////////////////////////////////////////////
/**
* Converts for the specified element, its stroke paint properties
* to a Paint object.
*
* @param strokedElement the element interested in a Paint
* @param strokedNode the graphics node to stroke
* @param ctx the bridge context
*/
public static Paint convertStrokePaint(Element strokedElement,
GraphicsNode strokedNode,
BridgeContext ctx) {
Value v = CSSUtilities.getComputedStyle
(strokedElement, SVGCSSEngine.STROKE_OPACITY_INDEX);
float opacity = convertOpacity(v);
v = CSSUtilities.getComputedStyle
(strokedElement, SVGCSSEngine.STROKE_INDEX);
return convertPaint(strokedElement,
strokedNode,
v,
opacity,
ctx);
}
/**
* Converts for the specified element, its fill paint properties
* to a Paint object.
*
* @param filledElement the element interested in a Paint
* @param filledNode the graphics node to fill
* @param ctx the bridge context
*/
public static Paint convertFillPaint(Element filledElement,
GraphicsNode filledNode,
BridgeContext ctx) {
Value v = CSSUtilities.getComputedStyle
(filledElement, SVGCSSEngine.FILL_OPACITY_INDEX);
float opacity = convertOpacity(v);
v = CSSUtilities.getComputedStyle
(filledElement, SVGCSSEngine.FILL_INDEX);
return convertPaint(filledElement,
filledNode,
v,
opacity,
ctx);
}
/**
* Converts a Paint definition to a concrete <code>java.awt.Paint</code>
* instance according to the specified parameters.
*
* @param paintedElement the element interested in a Paint
* @param paintedNode the graphics node to paint (objectBoundingBox)
* @param paintDef the paint definition
* @param opacity the opacity to consider for the Paint
* @param ctx the bridge context
*/
public static Paint convertPaint(Element paintedElement,
GraphicsNode paintedNode,
Value paintDef,
float opacity,
BridgeContext ctx) {
if (paintDef.getCssValueType() == CSSValue.CSS_PRIMITIVE_VALUE) {
switch (paintDef.getPrimitiveType()) {
case CSSPrimitiveValue.CSS_IDENT:
return null; // none
case CSSPrimitiveValue.CSS_RGBCOLOR:
return convertColor(paintDef, opacity);
case CSSPrimitiveValue.CSS_URI:
return convertURIPaint(paintedElement,
paintedNode,
paintDef,
opacity,
ctx);
default:
throw new IllegalArgumentException
("Paint argument is not an appropriate CSS value");
}
} else { // List
Value v = paintDef.item(0);
switch (v.getPrimitiveType()) {
case CSSPrimitiveValue.CSS_RGBCOLOR:
return convertRGBICCColor(paintedElement, v,
paintDef.item(1),
opacity, ctx);
case CSSPrimitiveValue.CSS_URI: {
Paint result = silentConvertURIPaint(paintedElement,
paintedNode,
v, opacity, ctx);
if (result != null) return result;
v = paintDef.item(1);
switch (v.getPrimitiveType()) {
case CSSPrimitiveValue.CSS_IDENT:
return null; // none
case CSSPrimitiveValue.CSS_RGBCOLOR:
if (paintDef.getLength() == 2) {
return convertColor(v, opacity);
} else {
return convertRGBICCColor(paintedElement, v,
paintDef.item(2),
opacity, ctx);
}
default:
throw new IllegalArgumentException
("Paint argument is not an appropriate CSS value");
}
}
default:
// can't be reached
throw new IllegalArgumentException
("Paint argument is not an appropriate CSS value");
}
}
}
/**
* Converts a Paint specified by URI without sending any error.
* if a problem occured while processing the URI, it just returns
* null (same effect as 'none')
*
* @param paintedElement the element interested in a Paint
* @param paintedNode the graphics node to paint (objectBoundingBox)
* @param paintDef the paint definition
* @param opacity the opacity to consider for the Paint
* @param ctx the bridge context
* @return the paint object or null when impossible
*/
public static Paint silentConvertURIPaint(Element paintedElement,
GraphicsNode paintedNode,
Value paintDef,
float opacity,
BridgeContext ctx) {
Paint paint = null;
try {
paint = convertURIPaint(paintedElement, paintedNode,
paintDef, opacity, ctx);
} catch (BridgeException ex) {
}
return paint;
}
/**
* Converts a Paint specified as a URI.
*
* @param paintedElement the element interested in a Paint
* @param paintedNode the graphics node to paint (objectBoundingBox)
* @param paintDef the paint definition
* @param opacity the opacity to consider for the Paint
* @param ctx the bridge context
*/
public static Paint convertURIPaint(Element paintedElement,
GraphicsNode paintedNode,
Value paintDef,
float opacity,
BridgeContext ctx) {
String uri = paintDef.getStringValue();
Element paintElement = ctx.getReferencedElement(paintedElement, uri);
Bridge bridge = ctx.getBridge(paintElement);
if (bridge == null || !(bridge instanceof PaintBridge)) {
throw new BridgeException
(ctx, paintedElement, ERR_CSS_URI_BAD_TARGET,
new Object[] {uri});
}
return ((PaintBridge)bridge).createPaint(ctx,
paintElement,
paintedElement,
paintedNode,
opacity);
}
/**
* Returns a Color object that corresponds to the input Paint's
* ICC color value or an RGB color if the related color profile
* could not be used or loaded for any reason.
*
* @param paintedElement the element using the color
* @param colorDef the color definition
* @param iccColor the ICC color definition
* @param opacity the opacity
* @param ctx the bridge context to use
*/
public static Color convertRGBICCColor(Element paintedElement,
Value colorDef,
Value iccColor,
float opacity,
BridgeContext ctx) {
Color color = null;
if (iccColor != null) {
if (iccColor instanceof ICCColor) {
color = convertICCColor(paintedElement, (ICCColor)iccColor, opacity, ctx);
} else if (iccColor instanceof ICCNamedColor) {
color = convertICCNamedColor(paintedElement, (ICCNamedColor)iccColor, opacity, ctx);
} else if (iccColor instanceof CIELabColor) {
color = convertCIELabColor(paintedElement, (CIELabColor)iccColor, opacity, ctx);
} else if (iccColor instanceof DeviceColor) {
color = convertDeviceColor(paintedElement, colorDef, (DeviceColor)iccColor, opacity, ctx);
}
}
if (color == null){
color = convertColor(colorDef, opacity);
}
return color;
}
/**
* Returns a Color object that corresponds to the input Paint's
* ICC color value or null if the related color profile could not
* be used or loaded for any reason.
*
* @param e the element using the color
* @param c the ICC color definition
* @param opacity the opacity
* @param ctx the bridge context to use
*/
public static Color convertICCColor(Element e,
ICCColor c,
float opacity,
BridgeContext ctx) {
// Get ICC Profile's name
String iccProfileName = c.getColorProfile();
if (iccProfileName == null){
return null;
}
// Ask the bridge to map the ICC profile name to an ICC_Profile object
SVGColorProfileElementBridge profileBridge
= (SVGColorProfileElementBridge)
ctx.getBridge(SVG_NAMESPACE_URI, SVG_COLOR_PROFILE_TAG);
if (profileBridge == null){
return null; // no bridge for color profile
}
ICCColorSpaceWithIntent profileCS
= profileBridge.createICCColorSpaceWithIntent(ctx, e, iccProfileName);
if (profileCS == null){
return null; // no profile
}
// Now, convert the colors to an array of floats
int n = c.getNumberOfColors();
float[] colorValue = new float[n];
if (n == 0) {
return null;
}
for (int i = 0; i < n; i++) {
colorValue[i] = c.getColor(i);
}
// Convert values to RGB
float[] rgb = profileCS.intendedToRGB(colorValue);
//TODO Preserve original ICC color value!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
return new Color(rgb[0], rgb[1], rgb[2], opacity);
}
/**
* Returns a Color object that corresponds to the input Paint's
* ICC named color value or null if the related color profile could not
* be used or loaded for any reason.
*
* @param e the element using the color
* @param c the ICC named color definition
* @param opacity the opacity
* @param ctx the bridge context to use
*/
public static Color convertICCNamedColor(Element e,
ICCNamedColor c,
float opacity,
BridgeContext ctx) {
// Get ICC Profile's name
String iccProfileName = c.getColorProfile();
if (iccProfileName == null){
return null;
}
// Ask the bridge to map the ICC profile name to an ICC_Profile object
SVGColorProfileElementBridge profileBridge
= (SVGColorProfileElementBridge)
ctx.getBridge(SVG_NAMESPACE_URI, SVG_COLOR_PROFILE_TAG);
if (profileBridge == null){
return null; // no bridge for color profile
}
ICCColorSpaceWithIntent profileCS
= profileBridge.createICCColorSpaceWithIntent(ctx, e, iccProfileName);
if (profileCS == null){
return null; // no profile
}
ICC_Profile iccProfile = profileCS.getProfile();
String iccProfileSrc = null; //TODO Fill me!
if (NamedColorProfileParser.isNamedColorProfile(iccProfile)) {
NamedColorProfileParser parser = new NamedColorProfileParser();
NamedColorProfile ncp;
try {
ncp = parser.parseProfile(iccProfile, iccProfileName, iccProfileSrc);
} catch (IOException ioe) {
return null;
}
NamedColorSpace ncs = ncp.getNamedColor(c.getColorName());
if (ncs != null) {
Color specColor
= new ColorWithAlternatives(ncs, new float[] {1.0f}, opacity, null);
return specColor;
} else {
/*
log.warn("Color '" + colorName
+ "' does not exist in named color profile: " + iccProfileSrc);
*/
}
} else {
//log.warn("ICC profile is no named color profile: " + iccProfileSrc);
}
return null;
}
/**
* Returns a Color object that corresponds to the input Paint's
* CIE Lab color value.
*
* @param e the element using the color
* @param c the CIE Lab color definition
* @param opacity the opacity
* @param ctx the bridge context to use
*/
public static Color convertCIELabColor(Element e,
CIELabColor c,
float opacity,
BridgeContext ctx) {
CIELabColorSpace cs = new CIELabColorSpace(c.getWhitePoint());
float[] lab = c.getColorValues();
Color specColor = cs.toColor(lab[0], lab[1], lab[2], opacity);
return specColor;
}
/**
* Returns a Color object that corresponds to the input Paint's
* device-specific color value.
*
* @param e the element using the color
* @param srgb the sRGB fallback color
* @param c the device-specific color definition
* @param opacity the opacity
* @param ctx the bridge context to use
*/
public static Color convertDeviceColor(Element e,
Value srgb,
DeviceColor c,
float opacity,
BridgeContext ctx) {
int r = resolveColorComponent(srgb.getRed());
int g = resolveColorComponent(srgb.getGreen());
int b = resolveColorComponent(srgb.getBlue());
if (c.isNChannel()) {
return convertColor(srgb, opacity); //NYI
} else {
if (c.getNumberOfColors() == 4) {
DeviceCMYKColorSpace cmykCs = ColorSpaces.getDeviceCMYKColorSpace();
float[] comps = new float[4];
for (int i = 0; i < 4; i++) {
comps[i] = c.getColor(i);
}
Color cmyk = new ColorWithAlternatives(cmykCs, comps, opacity, null);
Color specColor = new ColorWithAlternatives(r, g, b, Math.round(opacity * 255f),
new Color[] {cmyk});
return specColor;
} else {
return convertColor(srgb, opacity); //NYI
}
}
}
/**
* Converts the given Value and opacity to a Color object.
* @param c The CSS color to convert.
* @param opacity The opacity value (0 &lt;= o &lt;= 1).
*/
public static Color convertColor(Value c, float opacity) {
int r = resolveColorComponent(c.getRed());
int g = resolveColorComponent(c.getGreen());
int b = resolveColorComponent(c.getBlue());
return new Color(r, g, b, Math.round(opacity * 255f));
}
/////////////////////////////////////////////////////////////////////////
// java.awt.stroke
/////////////////////////////////////////////////////////////////////////
/**
* Converts a <code>Stroke</code> object defined on the specified element.
*
* @param e the element on which the stroke is specified
*/
public static Stroke convertStroke(Element e) {
Value v;
v = CSSUtilities.getComputedStyle
(e, SVGCSSEngine.STROKE_WIDTH_INDEX);
float width = v.getFloatValue();
if (width == 0.0f)
return null; // Stop here no stroke should be painted.
v = CSSUtilities.getComputedStyle
(e, SVGCSSEngine.STROKE_LINECAP_INDEX);
int linecap = convertStrokeLinecap(v);
v = CSSUtilities.getComputedStyle
(e, SVGCSSEngine.STROKE_LINEJOIN_INDEX);
int linejoin = convertStrokeLinejoin(v);
v = CSSUtilities.getComputedStyle
(e, SVGCSSEngine.STROKE_MITERLIMIT_INDEX);
float miterlimit = convertStrokeMiterlimit(v);
v = CSSUtilities.getComputedStyle
(e, SVGCSSEngine.STROKE_DASHARRAY_INDEX);
float[] dasharray = convertStrokeDasharray(v);
float dashoffset = 0;
if (dasharray != null) {
v = CSSUtilities.getComputedStyle
(e, SVGCSSEngine.STROKE_DASHOFFSET_INDEX);
dashoffset = v.getFloatValue();
// make the dashoffset positive since BasicStroke cannot handle
// negative values
if ( dashoffset < 0 ) {
float dashpatternlength = 0;
for (float aDasharray : dasharray) {
dashpatternlength += aDasharray;
}
// if the dash pattern consists of an odd number of elements,
// the pattern length must be doubled
if ( (dasharray.length % 2) != 0 )
dashpatternlength *= 2;
if (dashpatternlength ==0) {
dashoffset=0;
} else {
while (dashoffset < 0)
dashoffset += dashpatternlength;
}
}
}
return new BasicStroke(width,
linecap,
linejoin,
miterlimit,
dasharray,
dashoffset);
}
/////////////////////////////////////////////////////////////////////////
// Stroke utility methods
/////////////////////////////////////////////////////////////////////////
/**
* Converts the 'stroke-dasharray' property to a list of float
* number in user units.
*
* @param v the CSS value describing the dasharray property
*/
public static float [] convertStrokeDasharray(Value v) {
float [] dasharray = null;
if (v.getCssValueType() == CSSValue.CSS_VALUE_LIST) {
int length = v.getLength();
dasharray = new float[length];
float sum = 0;
for (int i = 0; i < dasharray.length; ++i) {
dasharray[i] = v.item(i).getFloatValue();
sum += dasharray[i];
}
if (sum == 0) {
/* 11.4 - If the sum of the <length>'s is zero, then
* the stroke is rendered as if a value of none were specified.
*/
dasharray = null;
}
}
return dasharray;
}
/**
* Converts the 'miterlimit' property to the appropriate float number.
* @param v the CSS value describing the miterlimit property
*/
public static float convertStrokeMiterlimit(Value v) {
float miterlimit = v.getFloatValue();
return (miterlimit < 1.0f) ? 1.0f : miterlimit;
}
/**
* Converts the 'linecap' property to the appropriate BasicStroke constant.
* @param v the CSS value describing the linecap property
*/
public static int convertStrokeLinecap(Value v) {
String s = v.getStringValue();
switch (s.charAt(0)) {
case 'b':
return BasicStroke.CAP_BUTT;
case 'r':
return BasicStroke.CAP_ROUND;
case 's':
return BasicStroke.CAP_SQUARE;
default:
throw new IllegalArgumentException
("Linecap argument is not an appropriate CSS value");
}
}
/**
* Converts the 'linejoin' property to the appropriate BasicStroke
* constant.
* @param v the CSS value describing the linejoin property
*/
public static int convertStrokeLinejoin(Value v) {
String s = v.getStringValue();
switch (s.charAt(0)) {
case 'm':
return BasicStroke.JOIN_MITER;
case 'r':
return BasicStroke.JOIN_ROUND;
case 'b':
return BasicStroke.JOIN_BEVEL;
default:
throw new IllegalArgumentException
("Linejoin argument is not an appropriate CSS value");
}
}
/////////////////////////////////////////////////////////////////////////
// Paint utility methods
/////////////////////////////////////////////////////////////////////////
/**
* Returns the value of one color component (0 <= result <= 255).
* @param v the value that defines the color component
*/
public static int resolveColorComponent(Value v) {
float f;
switch(v.getPrimitiveType()) {
case CSSPrimitiveValue.CSS_PERCENTAGE:
f = v.getFloatValue();
f = (f > 100f) ? 100f : (f < 0f) ? 0f : f;
return Math.round(255f * f / 100f);
case CSSPrimitiveValue.CSS_NUMBER:
f = v.getFloatValue();
f = (f > 255f) ? 255f : (f < 0f) ? 0f : f;
return Math.round(f);
default:
throw new IllegalArgumentException
("Color component argument is not an appropriate CSS value");
}
}
/**
* Returns the opacity represented by the specified CSSValue.
* @param v the value that represents the opacity
* @return the opacity between 0 and 1
*/
public static float convertOpacity(Value v) {
float r = v.getFloatValue();
return (r < 0f) ? 0f : (r > 1.0f) ? 1.0f : r;
}
}