/*
* Created on Oct 17, 2007, 6:07:17 PM
*
* MusicPageAnnotation.java
*
* Copyright (C) 2006-2007 Gabriel Burca (gburca dash virtmus at ebixio dot com)
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program 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 for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package com.ebixio.virtmus;
import com.ebixio.util.Log;
import com.ebixio.virtmus.imgsrc.PdfImg;
import com.ebixio.virtmus.shapes.VmShape;
import com.ebixio.virtmus.svg.SvgGenerator;
import com.thoughtworks.xstream.annotations.XStreamAlias;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.geom.Dimension2D;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.util.ArrayList;
import java.util.List;
import javax.swing.event.ChangeEvent;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.apache.batik.bridge.BridgeContext;
import org.apache.batik.bridge.GVTBuilder;
import org.apache.batik.bridge.UserAgentAdapter;
import org.apache.batik.dom.svg.SAXSVGDocumentFactory;
import org.apache.batik.gvt.CanvasGraphicsNode;
import org.apache.batik.gvt.CompositeGraphicsNode;
import org.apache.batik.gvt.GVTTreeWalker;
import org.apache.batik.gvt.GraphicsNode;
import org.apache.batik.svggen.SVGGraphics2D;
import org.apache.batik.util.XMLResourceDescriptor;
import org.openide.util.Exceptions;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.svg.SVGDocument;
/**
*
* @author Gabriel Burca <gburca dash virtmus at ebixio dot com>
*/
@XStreamAlias("page")
public class MusicPageSVG extends MusicPage {
/** Annotations shapes that have not yet been transfered to the svgDocument */
private transient ArrayList<VmShape> shapes = new ArrayList<VmShape>();
/** The DOM SVG document */
protected transient SVGDocument svgDocument = null;
/** The graphics node of the SVG annotations */
private transient GraphicsNode graphicsNode = null;
/** This field contains the SVG document string. It is only updated when
* prepareToSave() is called and should be considered invalid at all other times.
*/
private String annotationSVG = null;
/**
* This is the "id" attribute of the background "image" element that we add
* to exported SVG files and delete from imported SVG files.
*/
public transient final static String SVG_BACKGROUND_ID = "VirtMusBackground";
public MusicPageSVG(Song song, File sourceFile, Object opt) {
super(song, sourceFile, opt);
}
@Override
public void deserialize(Song s) {
super.deserialize(s);
shapes = new ArrayList<VmShape>();
setAnnotationSVG(this.annotationSVG, false);
}
public void setAnnotationSVG(String svgStr, boolean flagAsDirty) {
setAnnotationSVG(str2Document(svgStr), flagAsDirty);
}
public void setAnnotationSVG(SVGDocument svgDoc, boolean flagAsDirty) {
shapes.clear();
graphicsNode = null;
svgDocument = svgDoc;
if (flagAsDirty) {
setDirty(true);
if (this.changeListener != null) {
changeListener.stateChanged(new ChangeEvent(this));
}
}
song.notifyListeners();
}
@Override
public void prepareToSave() {
if (!hasAnnotations()) {
this.annotationSVG = null;
} else {
transferShapes2Doc();
String svg = document2Str(svgDocument);
clearAnnotations();
this.annotationSVG = svg;
setAnnotationSVG(svg, false);
}
}
protected SVGDocument addImgBackground(String svgStr) {
return addImgBackground(MusicPageSVG.str2Document(svgStr));
}
/**
* We add an "image" background to the SVG so that the user can edit the SVG
* externally.
*
* When importing back in we will remove the background image.
* @param doc The DOM document to add the background image to.
* @return The updated DOM document.
*/
protected SVGDocument addImgBackground(SVGDocument doc) {
try {
if (doc == null) {
return doc;
}
File imgFile = imgSrc.createImageFile();
if (imgFile == null) return doc;
Dimension imgDim = imgSrc.getDimension();
Element img = doc.createElement("image");
img.setAttribute("xlink:href", imgFile.getCanonicalPath());
img.setAttribute("width", Integer.toString(imgDim.width));
img.setAttribute("height", Integer.toString(imgDim.height));
img.setAttribute("x", Integer.toString(0));
img.setAttribute("y", Integer.toString(0));
img.setAttribute("id", MusicPageSVG.SVG_BACKGROUND_ID);
Element root = doc.getDocumentElement(); // <svg>
if (root != null) {
Node firstChild = root.getFirstChild();
if (firstChild != null) {
root.insertBefore(img, firstChild);
} else {
root.appendChild(img);
}
} else {
// We should never be in this situation.
root = doc.createElement("svg");
root.appendChild(img);
doc.appendChild(root);
}
// If we created a new document, it won't have width/height
if (!root.hasAttribute("width")) {
root.setAttribute("width", Integer.toString(imgDim.width));
}
if (!root.hasAttribute("height")) {
root.setAttribute("height", Integer.toString(imgDim.height));
}
return doc;
} catch (IOException ex) {
Exceptions.printStackTrace(ex);
}
return doc;
}
protected SVGDocument removeImgBackground(SVGDocument doc) {
if (doc == null) return doc;
Element root = doc.getDocumentElement(); // <svg>
NodeList images = root.getElementsByTagName("image");
for (int i = 0; i < images.getLength(); i++) {
Node n = images.item(i);
if (n.getNodeType() == Node.ELEMENT_NODE) {
Element image = (Element)n;
String id = image.getAttribute("id");
if (id.compareTo(MusicPageSVG.SVG_BACKGROUND_ID) == 0) {
root.removeChild(n);
}
}
}
return doc;
}
public void importSVG(File fromFile) {
if (fromFile == null) return;
if (!(fromFile.exists() && fromFile.canRead())) return;
SVGDocument doc = MusicPageSVG.getSVGDocument(fromFile);
removeImgBackground(doc);
setAnnotationSVG(doc, true);
}
/**
* Generates an SVG document that can be edited with external SVG editors.
* @return An SVG document.
*/
public String export2SvgStr() {
String svg;
SVGDocument document;
prepareToSave();
if (this.annotationSVG != null) {
document = addImgBackground(this.annotationSVG);
} else {
// Create empty document
SvgGenerator gen = new SvgGenerator();
document = addImgBackground(gen.getSVG());
}
svg = MusicPageSVG.document2Str(document);
return (svg == null) ? "" : svg;
}
/**
* Generates an SVG document that can be edited with external SVG editors.
* @param toFile The SVG file to export to. This file will be overwritten.
*/
public void export2SVG(File toFile) {
if (toFile == null) return;
OutputStreamWriter out = null;
try {
out = new OutputStreamWriter(new FileOutputStream(toFile), "UTF-8");
out.write(export2SvgStr());
} catch (IOException ex) {
Log.log(ex);
} finally {
try {
if (out != null) out.close();
} catch (IOException ex) {
Log.log(ex);
}
}
}
/**
* Launches an external SVG editor to edit an SVG file. After the external
* editor exits, it asks the MusicPage to load the edited SVG file as its
* annotation.
* @param editorPath The external editor to use
*/
synchronized public void externalSvgEdit(String editorPath) {
try {
File svgFile = File.createTempFile("VirtMus", ".svg");
export2SVG(svgFile);
List<String> command = new ArrayList<String>();
//command.add("c:\\Program Files\\Inkscape\\inkscape.exe");
command.add(editorPath);
//command.add("-f");
command.add(svgFile.getCanonicalPath());
ProcessBuilder builder = new ProcessBuilder(command);
//Map<String, String> environ = builder.environment();
//builder.directory(new File(System.getenv("temp")));
builder.directory(svgFile.getParentFile());
builder.redirectErrorStream(true);
//System.out.println("Directory : " + System.getenv("temp"));
final Process process = builder.start();
InputStream is = process.getInputStream();
InputStreamReader isr = new InputStreamReader(is);
BufferedReader br = new BufferedReader(isr);
String line;
while ((line = br.readLine()) != null) {
Log.log(line);
}
Log.log("SVG editor program terminated!");
if (svgFile.canRead()) {
importSVG(svgFile);
}
if (svgFile.canWrite()) {
svgFile.delete();
}
} catch (IOException ex) {
Exceptions.printStackTrace(ex);
}
imgSrc.destroyImageFile();
}
@Override
public void clearAnnotations() {
for (VmShape s: shapes) {
song.removedAnnot(this, s);
}
shapes.clear();
svgDocument = null;
graphicsNode = null;
setDirty(true);
song.notifyListeners();
}
@Override
public void popAnnotation() {
if (!shapes.isEmpty()) {
int idx = shapes.size() - 1;
song.removedAnnot(this, shapes.get(idx));
shapes.remove(idx);
setDirty(true);
song.notifyListeners();
}
}
@Override
public void addAnnotation(VmShape s) {
shapes.add(s);
setDirty(true);
song.addedAnnot(this, s);
song.notifyListeners();
}
/**
* Checks to see if there are any annotations available (visible on the page).
* The annotations could come from the SVG in the song.xml file, or from
* drawings done by the user on the page/canvas.
* @return <code>true</code> if the page contains any annotations.
*/
public boolean hasAnnotations() {
return !(svgDocument == null && shapes.isEmpty());
}
/**
* Paints all the annotations for this page on the Graphics2D object passed in.
* @param g2d The graphics to paint the annotations on.
*/
@Override
public void paintAnnotations(Graphics2D g2d) {
if (graphicsNode == null) {
updateGraphicsNode();
}
if (graphicsNode != null) {
graphicsNode.paint(g2d);
}
for (VmShape s: shapes) {
s.paint(g2d);
}
}
public static Node cloneNodeRec(Node source, Document targetDoc) {
String nodeName;
int sourceType = source.getNodeType();
Node result = null;
NamedNodeMap attr;
NodeList children;
switch (sourceType) {
case Node.DOCUMENT_NODE:
break;
case Node.ELEMENT_NODE:
nodeName = source.getNodeName();
Element resultE = targetDoc.createElement(nodeName);
NamedNodeMap attrs = source.getAttributes();
for(int i = 0; i < attrs.getLength();i++) {
Node current = attrs.item(i);
resultE.setAttributeNode((Attr)targetDoc.importNode(attrs.item(i), true));
}
children = source.getChildNodes();
if(children != null) {
for (int i = 0; i < children.getLength(); i++) {
resultE.appendChild(cloneNodeRec(children.item(i), targetDoc));
}
}
result = (Node) resultE;
break;
case Node.TEXT_NODE:
Node txtNode = targetDoc.createTextNode(source.getNodeValue());
result = txtNode;
break;
default:
nodeName = source.getNodeName();
Element resultD = targetDoc.createElement(nodeName);
NamedNodeMap attrs2 = source.getAttributes();
for(int i = 0; i < attrs2.getLength();i++) {
Node current = attrs2.item(i);
resultD.setAttributeNode((Attr)targetDoc.importNode(attrs2.item(i), true));
}
result = (Node) resultD;
// no children!!
}
return result;
}
/**
* Updates the existing SVG document with the new annotations (if any) from
* the com.ebixio.virtmus.shapes.* objects in the <b>shapes</b> array.
*/
public void transferShapes2Doc() {
if (shapes.isEmpty()) return;
SvgGenerator svgGenerator = new SvgGenerator();
SVGGraphics2D svgGraphics2D = svgGenerator.getGraphics();
// The dimensions of the SVG page (should match the size of the
// music page image the annotations were drawn on).
svgGraphics2D.setSVGCanvasSize(imgSrc.getDimension());
for (VmShape s: shapes) {
s.paint(svgGraphics2D);
}
shapes.clear();
SVGDocument newSvgDoc = str2Document(svgGenerator.getSVG());
if (svgDocument == null) {
svgDocument = newSvgDoc;
} else {
// The root <svg> of the existing document.
// We will append new shapes to this document.
Element svgRoot = svgDocument.getRootElement();
Element element = newSvgDoc.getRootElement(); // The <svg> element
NodeList children = element.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
Node child = children.item(i);
if (child.getNodeType() != Node.COMMENT_NODE) {
if ( !child.getNodeName().equals("defs") ) {
Node dup = cloneNodeRec(child, svgDocument);
svgRoot.appendChild(dup);
}
}
}
}
}
@Override
public MusicPageSVG clone() {
return this.clone(this.song);
}
@Override
public MusicPageSVG clone(Song song) {
MusicPageSVG mp;
if (imgSrc.getClass() == PdfImg.class) {
PdfImg pdf = (PdfImg)imgSrc;
mp = new MusicPageSVG(song, imgSrc.getSourceFile(), pdf.getPageNum());
} else {
mp = new MusicPageSVG(song, imgSrc.getSourceFile(), null);
}
mp.setName(this.getName());
mp.rotation = this.rotation;
prepareToSave();
mp.setAnnotationSVG(this.annotationSVG, false);
return mp;
}
/**
* Converts (serializes) a w3c Document to string
* @param doc The document to serialize
* @return An XML string that contains the document.
*/
static public String document2Str(Document doc) {
String docStr = null;
if (doc == null) return docStr;
TransformerFactory tFactory = TransformerFactory.newInstance();
try {
Transformer transformer = tFactory.newTransformer();
DOMSource source = new DOMSource(doc);
StreamResult result;
ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
result = new StreamResult(byteStream);
String SVG_MEDIA_TYPE = "image/svg+xml";
transformer.setOutputProperty(OutputKeys.MEDIA_TYPE, SVG_MEDIA_TYPE);
transformer.setOutputProperty(OutputKeys.METHOD, "xml");
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
transformer.transform(source, result);
docStr = byteStream.toString();
} catch (Exception ex) {
Exceptions.printStackTrace(ex);
}
return docStr;
}
static public SVGDocument str2Document(String src) {
SVGDocument document = null;
if (src == null) return document;
try {
// Load the document
String parser = XMLResourceDescriptor.getXMLParserClassName();
SAXSVGDocumentFactory f = new SAXSVGDocumentFactory(parser);
document = (SVGDocument) f.createDocument("file:///nosuchfile", new ByteArrayInputStream(src.getBytes()));
} catch (IOException ex) {
Exceptions.printStackTrace(ex);
}
return document;
}
@SuppressWarnings("deprecation")
static public SVGDocument getSVGDocument(File file) {
SVGDocument document = null;
try {
// Load the document
String parser = XMLResourceDescriptor.getXMLParserClassName();
SAXSVGDocumentFactory f = new SAXSVGDocumentFactory(parser);
document = (SVGDocument) f.createDocument(file.toURL().toString());
} catch (Exception ex) {
Exceptions.printStackTrace(ex);
}
return document;
}
protected void updateGraphicsNode() {
if (svgDocument == null) {
graphicsNode = null;
return;
}
// Build the tree and get the document dimensions
UserAgentAdapter userAgentAdapter;
Dimension dim = imgSrc.getDimension();
if (dim.width > 1 && dim.height > 1) {
// If the SVG document contains dimensions, which one has precedence,
// the SVG or the userAgentAdapter?
userAgentAdapter = new MyUserAgentAdapter(dim);
} else {
userAgentAdapter = new UserAgentAdapter();
}
BridgeContext bridgeContext = new BridgeContext(userAgentAdapter);
GVTBuilder builder = new GVTBuilder();
graphicsNode = builder.build(bridgeContext, svgDocument);
// CanvasGraphicsNode cgn = getCanvasGraphicsNode(graphicsNode);
// if (cgn != null) {
// cgn.setViewingTransform(new AffineTransform());
// }
}
protected CanvasGraphicsNode getCanvasGraphicsNode(GraphicsNode gn) {
if (!(gn instanceof CompositeGraphicsNode))
return null;
CompositeGraphicsNode cgn = (CompositeGraphicsNode)gn;
List children = cgn.getChildren();
if (children.isEmpty())
return null;
gn = (GraphicsNode)children.get(0);
if (!(gn instanceof CanvasGraphicsNode))
return null;
return (CanvasGraphicsNode)gn;
}
/**
* If the SVG document/file does not have dimensions:
* <code>
* <svg width="123" height="456" ... />
* </code>
* all the nodes will have a clip size of 1. This function deletes the clip.
*
* The real solution is to add dimensions to the SVG document or to use a
* UserAgentAdapter that provides a getViewportSize function which returns a
* size corresponding to the canvas that the SVG will be painted on (see for
* example: MyUserAgentAdapter).
*
* @see com.ebixio.virtmus.MusicPageAnnotations.MyUserAgentAdapter
*/
private void clearClip(GraphicsNode gNode) {
GVTTreeWalker treeWalker = new GVTTreeWalker(gNode);
GraphicsNode currNode;
while ((currNode = treeWalker.nextGraphicsNode()) != null) {
currNode.setClip(null);
}
}
protected class MyUserAgentAdapter extends UserAgentAdapter {
Dimension dim;
public MyUserAgentAdapter(Dimension dim) {
this.dim = dim;
}
@Override
public Dimension2D getViewportSize() {
return dim;
}
}
}