paper.js/src/svg/SVGImport.js
Jürg Lehni 878be7962e Merge branch 'refs/heads/master' into solve-cubic
Conflicts:
	src/path/Curve.js
	src/path/PathItem.Boolean.js
	src/util/Numerical.js
2015-01-02 15:33:23 +01:00

639 lines
26 KiB
JavaScript

/*
* Paper.js - The Swiss Army Knife of Vector Graphics Scripting.
* http://paperjs.org/
*
* Copyright (c) 2011 - 2014, Juerg Lehni & Jonathan Puckey
* http://scratchdisk.com/ & http://jonathanpuckey.com/
*
* Distributed under the MIT license. See LICENSE file for details.
*
* All rights reserved.
*/
/**
* A function scope holding all the functionality needed to convert a SVG DOM
* to a Paper.js DOM.
*/
new function() {
// Define a couple of helper functions to easily read values from SVG
// objects, dealing with baseVal, and item lists.
// index is option, and if passed, causes a lookup in a list.
function getValue(node, name, isString, allowNull) {
var namespace = SVGNamespaces[name],
value = namespace
? node.getAttributeNS(namespace, name)
: node.getAttribute(name);
if (value === 'null')
value = null;
// Interpret value as number. Never return NaN, but 0 instead.
// If the value is a sequence of numbers, parseFloat will
// return the first occurring number, which is enough for now.
return value == null
? allowNull
? null
: isString
? ''
: 0
: isString
? value
: parseFloat(value);
}
function getPoint(node, x, y, allowNull) {
x = getValue(node, x, false, allowNull);
y = getValue(node, y, false, allowNull);
return allowNull && (x == null || y == null) ? null
: new Point(x, y);
}
function getSize(node, w, h, allowNull) {
w = getValue(node, w, false, allowNull);
h = getValue(node, h, false, allowNull);
return allowNull && (w == null || h == null) ? null
: new Size(w, h);
}
// Converts a string attribute value to the specified type
function convertValue(value, type, lookup) {
return value === 'none'
? null
: type === 'number'
? parseFloat(value)
: type === 'array'
? value ? value.split(/[\s,]+/g).map(parseFloat) : []
: type === 'color'
? getDefinition(value) || value
: type === 'lookup'
? lookup[value]
: value;
}
// Importer functions for various SVG node types
function importGroup(node, type, options, isRoot) {
var nodes = node.childNodes,
isClip = type === 'clippath',
item = new Group(),
project = item._project,
currentStyle = project._currentStyle,
children = [];
if (!isClip) {
item = applyAttributes(item, node, isRoot);
// Style on items needs to be handled differently than all other
// items: We first apply the style to the item, then use it as the
// project's currentStyle, so it is used as a default for the
// creation of all nested items. importSVG then needs to check for
// items and avoid calling applyAttributes() again.
project._currentStyle = item._style.clone();
}
// Collect the children in an array and apply them all at once.
for (var i = 0, l = nodes.length; i < l; i++) {
var childNode = nodes[i],
child;
if (childNode.nodeType === 1
&& (child = importSVG(childNode, options, false))
&& !(child instanceof Symbol))
children.push(child);
}
item.addChildren(children);
// Clip paths are reduced (unboxed) and their attributes applied at the
// end.
if (isClip)
item = applyAttributes(item.reduce(), node, isRoot);
// Restore currentStyle
project._currentStyle = currentStyle;
if (isClip || type === 'defs') {
// We don't want the defs in the DOM. But we might want to use
// Symbols for them to save memory?
item.remove();
item = null;
}
return item;
}
function importPoly(node, type) {
var coords = node.getAttribute('points').match(
/[+-]?(?:\d*\.\d+|\d+\.?)(?:[eE][+-]?\d+)?/g),
points = [];
for (var i = 0, l = coords.length; i < l; i += 2)
points.push(new Point(
parseFloat(coords[i]),
parseFloat(coords[i + 1])));
var path = new Path(points);
if (type === 'polygon')
path.closePath();
return path;
}
function importPath(node) {
// Get the path data, and determine whether it is a compound path or a
// normal path based on the amount of moveTo commands inside it.
var data = node.getAttribute('d'),
param = { pathData: data };
// If there are multiple moveTo commands or a closePath command followed
// by other commands, we have a CompoundPath:
return (data.match(/m/gi) || []).length > 1 || /z\S+/i.test(data)
? new CompoundPath(param)
: new Path(param);
}
function importGradient(node, type) {
var id = (getValue(node, 'href', true) || '').substring(1),
isRadial = type === 'radialgradient',
gradient;
if (id) {
// Gradients are always wrapped in a Color object, so get the
// gradient object from there.
// TODO: Handle exception if there is no definition for this id.
gradient = definitions[id].getGradient();
} else {
var nodes = node.childNodes,
stops = [];
for (var i = 0, l = nodes.length; i < l; i++) {
var child = nodes[i];
if (child.nodeType === 1)
stops.push(applyAttributes(new GradientStop(), child));
}
gradient = new Gradient(stops, isRadial);
}
var origin, destination, highlight;
if (isRadial) {
origin = getPoint(node, 'cx', 'cy');
destination = origin.add(getValue(node, 'r'), 0);
highlight = getPoint(node, 'fx', 'fy', true);
} else {
origin = getPoint(node, 'x1', 'y1');
destination = getPoint(node, 'x2', 'y2');
}
applyAttributes(
new Color(gradient, origin, destination, highlight), node);
// We don't return the gradient, since we only need a reference to it in
// definitions, which is created in applyAttributes()
return null;
}
// NOTE: All importers are lowercase, since jsdom is using uppercase
// nodeNames still.
var importers = {
'#document': function (node, type, options, isRoot) {
var nodes = node.childNodes;
for (var i = 0, l = nodes.length; i < l; i++) {
var child = nodes[i];
if (child.nodeType === 1) {
// NOTE: We need to move the svg node into our current
// document, so default styles apply!
var next = child.nextSibling;
document.body.appendChild(child);
var item = importSVG(child, options, isRoot);
// After import, we move it back to where it was:
if (next) {
node.insertBefore(child, next);
} else {
node.appendChild(child);
}
return item;
}
}
},
// http://www.w3.org/TR/SVG/struct.html#Groups
g: importGroup,
// http://www.w3.org/TR/SVG/struct.html#NewDocument
svg: importGroup,
clippath: importGroup,
// http://www.w3.org/TR/SVG/shapes.html#PolygonElement
polygon: importPoly,
// http://www.w3.org/TR/SVG/shapes.html#PolylineElement
polyline: importPoly,
// http://www.w3.org/TR/SVG/paths.html
path: importPath,
// http://www.w3.org/TR/SVG/pservers.html#LinearGradients
lineargradient: importGradient,
// http://www.w3.org/TR/SVG/pservers.html#RadialGradients
radialgradient: importGradient,
// http://www.w3.org/TR/SVG/struct.html#ImageElement
image: function (node) {
var raster = new Raster(getValue(node, 'href', true));
raster.on('load', function() {
var size = getSize(node, 'width', 'height');
this.setSize(size);
// Since x and y start from the top left of an image, add
// half of its size. We also need to take the raster's matrix
// into account, which will be defined by the time the load
// event is called.
var center = this._matrix._transformPoint(
getPoint(node, 'x', 'y').add(size.divide(2)));
this.translate(center);
});
return raster;
},
// http://www.w3.org/TR/SVG/struct.html#SymbolElement
symbol: function(node, type, options, isRoot) {
// Pass true for dontCenter:
return new Symbol(importGroup(node, type, options, isRoot), true);
},
// http://www.w3.org/TR/SVG/struct.html#DefsElement
defs: importGroup,
// http://www.w3.org/TR/SVG/struct.html#UseElement
use: function(node) {
// Note the namespaced xlink:href attribute is just called href
// as a property on node.
// TODO: Support overflow and width, height, in combination with
// overflow: hidden. Paper.js currently does not support
// PlacedSymbol clipping, but perhaps it should?
var id = (getValue(node, 'href', true) || '').substring(1),
definition = definitions[id],
point = getPoint(node, 'x', 'y');
// Use place if we're dealing with a symbol:
return definition
? definition instanceof Symbol
// When placing symbols, we nee to take both point and
// matrix into account. This just does the right thing:
? definition.place(point)
: definition.clone().translate(point)
: null;
},
// http://www.w3.org/TR/SVG/shapes.html#InterfaceSVGCircleElement
circle: function(node) {
return new Shape.Circle(getPoint(node, 'cx', 'cy'),
getValue(node, 'r'));
},
// http://www.w3.org/TR/SVG/shapes.html#InterfaceSVGEllipseElement
ellipse: function(node) {
// We only use object literal notation where the default one is not
// supported (e.g. center / radius fo Shape.Ellipse).
return new Shape.Ellipse({
center: getPoint(node, 'cx', 'cy'),
radius: getSize(node, 'rx', 'ry')
});
},
// http://www.w3.org/TR/SVG/shapes.html#RectElement
rect: function(node) {
var point = getPoint(node, 'x', 'y'),
size = getSize(node, 'width', 'height'),
radius = getSize(node, 'rx', 'ry');
return new Shape.Rectangle(new Rectangle(point, size), radius);
},
// http://www.w3.org/TR/SVG/shapes.html#LineElement
line: function(node) {
return new Path.Line(getPoint(node, 'x1', 'y1'),
getPoint(node, 'x2', 'y2'));
},
text: function(node) {
// Not supported by Paper.js
// x: multiple values for x
// y: multiple values for y
// dx: multiple values for x
// dy: multiple values for y
// TODO: Support for these is missing in Paper.js right now
// rotate: character rotation
// lengthAdjust:
var text = new PointText(getPoint(node, 'x', 'y')
.add(getPoint(node, 'dx', 'dy')));
text.setContent(node.textContent.trim() || '');
return text;
}
};
// Attributes and Styles
// NOTE: Parameter sequence for all apply*() functions is:
// (item, value, name, node) rather than (item, node, name, value),
// so we can omit the less likely parameters from right to left.
function applyTransform(item, value, name, node) {
// http://www.w3.org/TR/SVG/types.html#DataTypeTransformList
// Parse SVG transform string. First we split at /)\s*/, to separate
// commands
var transforms = (node.getAttribute(name) || '').split(/\)\s*/g),
matrix = new Matrix();
for (var i = 0, l = transforms.length; i < l; i++) {
var transform = transforms[i];
if (!transform)
break;
// Command come before the '(', values after
var parts = transform.split(/\(\s*/),
command = parts[0],
v = parts[1].split(/[\s,]+/g);
// Convert values to floats
for (var j = 0, m = v.length; j < m; j++)
v[j] = parseFloat(v[j]);
switch (command) {
case 'matrix':
matrix.concatenate(
new Matrix(v[0], v[1], v[2], v[3], v[4], v[5]));
break;
case 'rotate':
matrix.rotate(v[0], v[1], v[2]);
break;
case 'translate':
matrix.translate(v[0], v[1]);
break;
case 'scale':
matrix.scale(v);
break;
case 'skewX':
matrix.skew(v[0], 0);
break;
case 'skewY':
matrix.skew(0, v[0]);
break;
}
}
item.transform(matrix);
}
function applyOpacity(item, value, name) {
// http://www.w3.org/TR/SVG/painting.html#FillOpacityProperty
// http://www.w3.org/TR/SVG/painting.html#StrokeOpacityProperty
var color = item[name === 'fill-opacity' ? 'getFillColor'
: 'getStrokeColor']();
if (color)
color.setAlpha(parseFloat(value));
}
// Create apply-functions for attributes, and merge in those for SVGStlyes.
// We need to define style attributes first, and merge in all others after,
// since transform needs to be applied after fill color, as transformations
// can affect gradient fills.
var attributes = Base.each(SVGStyles, function(entry) {
this[entry.attribute] = function(item, value) {
item[entry.set](convertValue(value, entry.type, entry.fromSVG));
// When applying gradient colors to shapes, we need to offset
// the shape's initial position to get the same results as SVG.
if (entry.type === 'color' && item instanceof Shape) {
// Do not use result of convertValue() above, since calling
// the setter will produce a new cloned color.
var color = item[entry.get]();
if (color)
color.transform(new Matrix().translate(
item.getPosition(true).negate()));
}
};
}, {
id: function(item, value) {
definitions[value] = item;
if (item.setName)
item.setName(value);
},
'clip-path': function(item, value) {
// http://www.w3.org/TR/SVG/masking.html#ClipPathProperty
var clip = getDefinition(value);
if (clip) {
clip = clip.clone();
clip.setClipMask(true);
// If item is already a group, move the clip-path inside
if (item instanceof Group) {
item.insertChild(0, clip);
} else {
return new Group(clip, item);
}
}
},
gradientTransform: applyTransform,
transform: applyTransform,
'fill-opacity': applyOpacity,
'stroke-opacity': applyOpacity,
visibility: function(item, value) {
item.setVisible(value === 'visible');
},
display: function(item, value) {
// NOTE: 'none' gets translated to null in getAttribute()
item.setVisible(value !== null);
},
'stop-color': function(item, value) {
// http://www.w3.org/TR/SVG/pservers.html#StopColorProperty
if (item.setColor)
item.setColor(value);
},
'stop-opacity': function(item, value) {
// http://www.w3.org/TR/SVG/pservers.html#StopOpacityProperty
// NOTE: It is important that this is applied after stop-color!
if (item._color)
item._color.setAlpha(parseFloat(value));
},
offset: function(item, value) {
// http://www.w3.org/TR/SVG/pservers.html#StopElementOffsetAttribute
var percentage = value.match(/(.*)%$/);
item.setRampPoint(percentage
? percentage[1] / 100
: parseFloat(value));
},
viewBox: function(item, value, name, node, styles) {
// http://www.w3.org/TR/SVG/coords.html#ViewBoxAttribute
// TODO: implement preserveAspectRatio attribute
// viewBox will be applied both to the group that's created for the
// content in Symbol.definition, and the Symbol itself.
var rect = new Rectangle(convertValue(value, 'array')),
size = getSize(node, 'width', 'height', true);
if (item instanceof Group) {
// This is either a top-level svg node, or the container for a
// symbol.
var scale = size ? rect.getSize().divide(size) : 1,
matrix = new Matrix().translate(rect.getPoint()).scale(scale);
item.transform(matrix.inverted());
} else if (item instanceof Symbol) {
// The symbol is wrapping a group. Note that viewBox was already
// applied to the group, and above code was executed for it.
// All that is left to handle here on the Symbol level is
// clipping. We can't do it at group level because
// applyAttributes() gets called for groups before their
// children are added, for styling reasons. See importGroup()
if (size)
rect.setSize(size);
var clip = getAttribute(node, 'overflow', styles) != 'visible',
group = item._definition;
if (clip && !rect.contains(group.getBounds())) {
// Add a clip path at the top of this symbol's group
clip = new Shape.Rectangle(rect).transform(group._matrix);
clip.setClipMask(true);
group.addChild(clip);
}
}
}
});
function getAttribute(node, name, styles) {
// First see if the given attribute is defined.
var attr = node.attributes[name],
value = attr && attr.value;
if (!value) {
// Fallback to using styles. See if there is a style, either set
// directly on the object or applied to it through CSS rules.
// We also need to filter out inheritance from their parents.
var style = Base.camelize(name);
value = node.style[style];
if (!value && styles.node[style] !== styles.parent[style])
value = styles.node[style];
}
// Return undefined if attribute is not defined, but null if it's
// defined as not set (e.g. fill / stroke).
return !value
? undefined
: value === 'none'
? null
: value;
}
/**
* Converts various SVG styles and attributes into Paper.js styles and
* attributes and applies them to the passed item.
*
* @param {SVGElement} node an SVG node to read style and attributes from.
* @param {Item} item the item to apply the style and attributes to.
*/
function applyAttributes(item, node, isRoot) {
// SVG attributes can be set both as styles and direct node attributes,
// so we need to handle both.
var styles = {
node: DomElement.getStyles(node) || {},
// Do not check for inheritance if this is the root, since we want
// the default SVG settings to stick.
parent: !isRoot && DomElement.getStyles(node.parentNode) || {}
};
Base.each(attributes, function(apply, name) {
var value = getAttribute(node, name, styles);
if (value !== undefined)
item = Base.pick(apply(item, value, name, node, styles), item);
});
return item;
}
var definitions = {};
function getDefinition(value) {
// When url() comes from a style property, '#'' seems to be missing on
// WebKit, so let's make it optional here:
var match = value && value.match(/\((?:#|)([^)']+)/);
return match && definitions[match[1]];
}
function importSVG(source, options, isRoot) {
if (!source)
return null;
if (!options) {
options = {};
} else if (typeof options === 'function') {
options = { onLoad: options };
}
var node = source,
// Remember current scope so we can restore it in onLoad.
scope = paper;
function onLoadCallback(svg) {
paper = scope;
var item = importSVG(svg, options, isRoot),
onLoad = options.onLoad,
view = scope.project && scope.getView();
if (onLoad)
onLoad.call(this, item);
view.update();
}
if (isRoot) {
// Have the group not pass on all transformations to its children,
// as this is how SVG works too.
// See if it's a string but handle markup separately
if (typeof source === 'string' && !/^.*</.test(source)) {
/*#*/ if (__options.environment == 'browser') {
// First see if we're meant to import an element with the given
// id.
node = document.getElementById(source);
// Check if the string does not represent SVG data, in which
// case it must be the URL of a SVG to be loaded.
if (node) {
source = null;
} else {
return Http.request('get', source, onLoadCallback);
}
/*#*/ } else if (__options.environment == 'node') {
// TODO: Implement!
/*#*/ } // __options.environment == 'node'
} else if (typeof File !== 'undefined' && source instanceof File) {
// Load local file through FileReader
var reader = new FileReader();
reader.onload = function() {
onLoadCallback(reader.result);
};
return reader.readAsText(source);
}
}
if (typeof source === 'string')
node = new DOMParser().parseFromString(source, 'image/svg+xml');
if (!node.nodeName)
throw new Error('Unsupported SVG source: ' + source);
// jsdom in Node.js uses uppercase values for nodeName...
var type = node.nodeName.toLowerCase(),
importer = importers[type],
item,
data = node.getAttribute && node.getAttribute('data-paper-data'),
settings = scope.settings,
applyMatrix = settings.applyMatrix;
// Have items imported from SVG not bake in all transformations to their
// content and children, as this is how SVG works too, but preserve the
// current setting so we can restore it after.
settings.applyMatrix = false;
item = importer && importer(node, type, options, isRoot) || null;
settings.applyMatrix = applyMatrix;
if (item) {
// Do not apply attributes if this is a #document node.
// See importGroup() for an explanation of filtering for Group:
if (type !== '#document' && !(item instanceof Group))
item = applyAttributes(item, node, isRoot);
// Support onImportItem callback, to provide mechanism to handle
// special attributes (e.g. inkscape:transform-center)
var onImport = options.onImport;
if (onImport)
item = onImport(node, item, options) || item;
if (options.expandShapes && item instanceof Shape) {
item.remove();
item = item.toPath();
}
if (data)
item._data = JSON.parse(data);
}
// Clear definitions at the end of import?
if (isRoot) {
definitions = {};
// Now if settings.applyMatrix was set, apply recursively and set
// #applyMatrix = true on the item and all children.
if (applyMatrix && item)
item.matrix.apply(true, true);
}
return item;
}
// NOTE: Documentation is in Item.js
Item.inject({
importSVG: function(node, options) {
return this.addChild(importSVG(node, options, true));
}
});
// NOTE: Documentation is in Project.js
Project.inject({
importSVG: function(node, options) {
this.activate();
return importSVG(node, options, true);
}
});
};