/** * */ package cz.cuni.mff.peckam.java.origamist.services; import java.awt.Color; import java.awt.Component; import java.awt.Container; import java.awt.Dimension; import java.awt.Font; import java.awt.Graphics2D; import java.awt.Image; import java.awt.Insets; import java.awt.Rectangle; import java.awt.image.BufferedImage; import java.io.File; import java.io.FileOutputStream; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.net.URI; import java.net.URL; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Queue; import java.util.Set; import java.util.concurrent.Callable; import javax.imageio.ImageIO; import javax.media.j3d.Canvas3D; import javax.swing.JPanel; import javax.swing.origamist.JMultilineLabel; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; import javax.xml.bind.MarshalException; import javax.xml.bind.Marshaller; import javax.xml.bind.UnmarshalException; import org.apache.batik.dom.GenericDOMImplementation; import org.apache.batik.svggen.SVGGeneratorContext; import org.apache.batik.svggen.SVGGraphics2D; import org.apache.batik.transcoder.TranscoderException; import org.apache.batik.transcoder.TranscoderInput; import org.apache.batik.transcoder.TranscoderOutput; import org.apache.fop.svg.PDFTranscoder; import org.apache.log4j.Logger; import org.w3c.dom.DOMImplementation; import org.w3c.dom.Document; import org.xml.sax.Attributes; import org.xml.sax.SAXException; import com.bric.qt.io.JPEGMovieAnimation; import com.itextpdf.text.DocumentException; import com.itextpdf.text.pdf.BadPdfFormatException; import com.itextpdf.text.pdf.PdfCopy; import com.itextpdf.text.pdf.PdfReader; import com.jgoodies.forms.layout.CellConstraints; import com.jgoodies.forms.layout.FormLayout; import com.sun.j3d.utils.universe.SimpleUniverse; import cz.cuni.mff.peckam.java.origamist.exceptions.UnsupportedDataFormatException; import cz.cuni.mff.peckam.java.origamist.gui.common.StepMorphingCanvasController; import cz.cuni.mff.peckam.java.origamist.jaxb.AdditionalTransforms.TransformLocation; import cz.cuni.mff.peckam.java.origamist.jaxb.BindingsController; import cz.cuni.mff.peckam.java.origamist.jaxb.BindingsManager; import cz.cuni.mff.peckam.java.origamist.jaxb.ParseAbortedException; import cz.cuni.mff.peckam.java.origamist.jaxb.ResultDelegatingDefaultHandler; import cz.cuni.mff.peckam.java.origamist.jaxb.TransformInfo; import cz.cuni.mff.peckam.java.origamist.model.Model; import cz.cuni.mff.peckam.java.origamist.model.ObjectFactory; import cz.cuni.mff.peckam.java.origamist.model.Origami; import cz.cuni.mff.peckam.java.origamist.model.Step; import cz.cuni.mff.peckam.java.origamist.model.UnitDimension; import cz.cuni.mff.peckam.java.origamist.model.jaxb.Unit; import cz.cuni.mff.peckam.java.origamist.services.interfaces.ConfigurationManager; import cz.cuni.mff.peckam.java.origamist.services.interfaces.OrigamiHandler; import cz.cuni.mff.peckam.java.origamist.utils.ExportFormat; import cz.cuni.mff.peckam.java.origamist.utils.ExportOptions; import cz.cuni.mff.peckam.java.origamist.utils.MOVExportOptions; import cz.cuni.mff.peckam.java.origamist.utils.PDFExportOptions; import cz.cuni.mff.peckam.java.origamist.utils.PNGExportOptions; import cz.cuni.mff.peckam.java.origamist.utils.SVGExportOptions; import cz.cuni.mff.peckam.java.origamist.utils.URIAdapter; /** * Loads an origami model from XML file using JAXB and vice versa. * * The code is inspired by partial-unmarshalling example in JAXB section of JWSDP. * * @author Martin Pecka */ public class JAXBOrigamiHandler extends Service implements OrigamiHandler { /** The namespace of the newest schema. */ public static final String LATEST_SCHEMA_NAMESPACE = "http://www.mff.cuni.cz/~peckam/java/origamist/diagram/v2"; /** The model to return. */ protected Origami model = null; /** The base path for resolving relative URIs. */ protected URL documentBase = null; public JAXBOrigamiHandler(URL documentBase) { this.documentBase = documentBase; } @Override public void save(Origami origami, File file) throws IOException, MarshalException, JAXBException { if (!file.exists()) file.createNewFile(); if (!file.isFile()) throw new IOException("Cannot save the model in a directory or a non-file object: " + file.getAbsolutePath() + "."); JAXBContext context = JAXBContext.newInstance("cz.cuni.mff.peckam.java.origamist.model.jaxb", getClass() .getClassLoader()); Marshaller m = context.createMarshaller(); // enable indenting and newline generation m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true); // make URLs in the listing relative to the location we save the model to m.setAdapter(new URIAdapter()); m.getAdapter(URIAdapter.class).setRelativeBase(file.getParentFile().toURI()); // do the Java class->XML conversion m.marshal(new ObjectFactory().createOrigami(origami), file); } @Override public Set<File> export(Origami origami, File file, ExportFormat format) throws IOException { return export(origami, file, format, null); } @Override public Set<File> export(Origami origami, File file, ExportFormat format, ExportOptions options) throws IOException { return export(origami, file, format, options, null); } @Override public Set<File> export(final Origami origami, File file, ExportFormat format, ExportOptions options, Runnable progressCallback) throws IOException { Set<File> result = new LinkedHashSet<File>(); if (format == ExportFormat.XML) { try { save(origami, file); } catch (MarshalException e) { throw new IOException(e); } catch (JAXBException e) { throw new IOException(e); } result.add(file); if (progressCallback != null) progressCallback.run(); } else if (format == ExportFormat.PNG || format == ExportFormat.SVG || format == ExportFormat.PDF) { // a lot of code is common for those 3 formats, so we won't divide them Double dpi = null; Locale locale = null; Insets pageInsets = null; boolean withBackground = true; // configure from options if (options != null) { if (options instanceof PNGExportOptions) { PNGExportOptions options2 = (PNGExportOptions) options; dpi = options2.getDpi(); locale = options2.getLocale(); withBackground = options2.isWithBackground(); pageInsets = options2.getPageInsets(); } else if (options instanceof SVGExportOptions) { SVGExportOptions options2 = (SVGExportOptions) options; dpi = options2.getDpi(); locale = options2.getLocale(); withBackground = options2.isWithBackground(); pageInsets = options2.getPageInsets(); } else if (options instanceof PDFExportOptions) { PDFExportOptions options2 = (PDFExportOptions) options; dpi = options2.getDpi(); locale = options2.getLocale(); withBackground = options2.isWithBackground(); pageInsets = options2.getPageInsets(); } else { dpi = 72d; locale = ServiceLocator.get(ConfigurationManager.class).get().getDiagramLocale(); pageInsets = new Insets(25, 25, 25, 25); } } else { dpi = 72d; locale = ServiceLocator.get(ConfigurationManager.class).get().getDiagramLocale(); pageInsets = new Insets(25, 25, 25, 25); } final File parentFile = (file.isDirectory() ? file : file.getParentFile()); final FileNameGenerator fileNames; if (!file.isDirectory()) { String prefix = file.getName().replaceAll("\\.[^.]*$", ""); String suffix = file.getName().replaceAll("^.*(\\.[^.]*)$", "$1"); fileNames = new FileNameGeneratorImpl(prefix, suffix, origami.getNumberOfPages()); } else { fileNames = new FileNameGeneratorImpl("", ".png", origami.getNumberOfPages()); } UnitDimension paperDim = origami.getPaper().getSize(); paperDim = paperDim.convertTo(Unit.INCH); final Dimension resultDim = new Dimension((int) (paperDim.getWidth() * dpi), (int) (paperDim.getHeight() * dpi)); // create the graphics object - either from a buffered image, or an SVG graphics final Graphics2D g; BufferedImage buffer = null; Document svgDocument = null; if (format == ExportFormat.PNG) { buffer = new BufferedImage(resultDim.width, resultDim.height, BufferedImage.TYPE_INT_ARGB); g = buffer.createGraphics(); } else { DOMImplementation domImpl = GenericDOMImplementation.getDOMImplementation(); String svgNS = "http://www.w3.org/2000/svg"; svgDocument = domImpl.createDocument(svgNS, "svg", null); SVGGeneratorContext ctx = SVGGeneratorContext.createDefault(svgDocument); // this is important, otherwise the SVG files use a different font and that breaks the result ctx.setEmbeddedFontsOn(true); g = new SVGGraphics2D(ctx, true); } Font font = new JMultilineLabel("").getFont(); font = font.deriveFont((float) (font.getSize2D() * dpi / 72)); // normalize the font size // iterate through pages, draw them into the created graphics, and write to files IOException e = null; for (int i = 0; i <= origami.getNumberOfPages(); i++) { try { if (i > 0) { // DRAW THE PAGE drawPage(g, new Rectangle(0, 0, resultDim.width, resultDim.height), pageInsets, origami, i, locale, font, withBackground, progressCallback); } else { // DRAW TITLE PAGE drawTitlePage(g, new Rectangle(0, 0, resultDim.width, resultDim.height), pageInsets, origami, locale, font, withBackground, progressCallback); } } catch (InterruptedException ex) { // allows us to cancel the computation g.dispose(); return result; } if (format == ExportFormat.PNG && buffer != null) { try { // write the image buffer to file buffer.flush(); File pageFile = new File(parentFile, fileNames.getFileName(i)); ImageIO.write(buffer, "png", pageFile); result.add(pageFile); } catch (IOException ex) { e = ex; } } else if (format == ExportFormat.SVG || format == ExportFormat.PDF) { // write the SVG graphics to file File outFile = new File(parentFile, fileNames.getFileName(i)); FileWriter writer = null; try { writer = new FileWriter(outFile); // PDF will be written with incorrect extension, but it doesn't matter since it is only // temporary ((SVGGraphics2D) g).stream(writer, true); result.add(outFile); } catch (IOException ex) { e = ex; } finally { if (writer != null) { try { writer.flush(); writer.close(); } catch (IOException ex) { e = ex; } } } } } g.dispose(); if (format == ExportFormat.PDF) { // coversion to PDF is a little tricky // although the manuals say that exporting PDF directly from SVG tree is possible, I didn't get correct // results - always just a blank PDF page; so we've written the SVGs to disk, loading them from there // works // SVG doesn't handle pages, nor Batik does, so we must write one file per page and then merge them // using iText PDFTranscoder trans = new PDFTranscoder(); trans.addTranscodingHint(PDFTranscoder.KEY_WIDTH, (float) resultDim.width); trans.addTranscodingHint(PDFTranscoder.KEY_HEIGHT, (float) resultDim.height); List<File> newResult = new LinkedList<File>(); for (File f : result) { // transcode SVGs to PDFs - one file per page TranscoderInput input = new TranscoderInput(new FileReader(f)); File outFile = new File(parentFile, f.getName() + ".pdf"); newResult.add(outFile); OutputStream stream = new FileOutputStream(outFile); TranscoderOutput output = new TranscoderOutput(stream); try { trans.transcode(input, output); newResult.add(outFile); } catch (TranscoderException e1) { e = new IOException(e1); } finally { try { stream.flush(); stream.close(); } catch (IOException e1) { e = e1; } progressCallback.run(); } } // delete the temporary SVG files for (File f : result) f.delete(); result.clear(); result.addAll(newResult); // initialize iText com.itextpdf.text.Document doc = new com.itextpdf.text.Document(new com.itextpdf.text.Rectangle( resultDim.width, resultDim.height)); PdfCopy copy = null; try { copy = new PdfCopy(doc, new FileOutputStream(file)); } catch (DocumentException e2) { e = new IOException(e2); } // merge the multiple pdf files into one if (copy != null) { doc.open(); PdfReader reader = null; int n; for (File f : result) { try { reader = new PdfReader(f.toString()); // loop over the pages in documents n = reader.getNumberOfPages(); for (int page = 0; page < n;) { try { copy.addPage(copy.getImportedPage(reader, ++page)); } catch (BadPdfFormatException e1) { e = new IOException(e1); } } } catch (IOException ex) { e = ex; } finally { if (reader != null) copy.freeReader(reader); progressCallback.run(); } } doc.close(); for (File f : result) f.delete(); result.clear(); result.add(file); } } // this way we try to create the most files, so one error in the middle won't break the rest of files if (e != null) throw e; } else if (format == ExportFormat.MOV) { MOVExportOptions opt = (MOVExportOptions) options; Canvas3D canvas = new Canvas3D(SimpleUniverse.getPreferredConfiguration(), true); canvas.setSize(opt.getSize()); final StepMorphingCanvasController morpher = new StepMorphingCanvasController(canvas, origami, origami .getModel().getSteps().getStep().get(0), (int) (opt.getFps() * opt.getStepDuration())); JPEGMovieAnimation anim = new JPEGMovieAnimation(file); int numFrames = (int) (origami.getModel().getSteps().getStep().size() * opt.getFps() * opt .getStepDuration()); int i = 0; BufferedImage im; while ((im = morpher.getNextFrame()) != null) { anim.addFrame((float) (1f / opt.getFps()), im, 1f); progressCallback.run(); if (i++ > numFrames) // we don't want an infinite loop when something goes bad break; } anim.close(); result.add(file); } else { Logger.getLogger("application").error("Unsupported export format: " + format); throw new IOException("Unsupported export format: " + format); } return result; } /** * Draw the given origami's title page to the given graphics into the given rectangle. * * @param graphics The graphics to draw to. * @param bounds The rectangle to draw into. * @param insets insets of the page. * @param origami The origami to draw. * @param locale The locale used to generate step descriptions. * @param font The font to draw text with. This is the base font size and will be enlarged for titles. * @param withBackground If true, fill background with diagram background color from the origami, else leave the * background transparent. * @param progressCallback The progress callback. */ protected void drawTitlePage(Graphics2D graphics, Rectangle bounds, Insets insets, Origami origami, Locale locale, Font font, boolean withBackground, Runnable progressCallback) throws InterruptedException { if (withBackground) graphics.setBackground(origami.getPaper().getBackgroundColor()); else graphics.setBackground(new Color(0, 0, 0, 0)); graphics.clearRect(0, 0, (int) bounds.getWidth(), (int) bounds.getHeight()); int width = bounds.width - insets.left - insets.right; int height = bounds.height - insets.top - insets.bottom; JMultilineLabel label = new JMultilineLabel(""); label.setOpaque(false); label.setFont(font); label.setVisible(true); StringBuilder text = new StringBuilder(); text.append("<html><center>"); text.append("<h1>").append(origami.getName(locale)).append("</h1>"); text.append("<h2>").append(origami.getShortDesc(locale)).append("</h2>"); text.append("<p>").append(origami.getDescription(locale)).append("</p>"); text.append("</center></html>"); label.setText(text.toString()); int labelHeight; { JPanel panel = new JPanel(); panel.setSize(4000, 4000); // this block computes the label's height when we know its fixed maximum width panel.setLayout(new FormLayout(width + "px", "default")); panel.add(label, new CellConstraints(1, 1)); Queue<Container> containers = new LinkedList<Container>(); containers.add(panel); Container container; while ((container = containers.poll()) != null) { container.doLayout(); for (Component c : container.getComponents()) { if (c instanceof Container) containers.add((Container) c); } } labelHeight = label.getSize().height; } label.setSize(width, labelHeight); StepThumbnailGenerator generator = ServiceLocator.get(StepThumbnailGenerator.class); Image image = generator.getThumbnail(origami, origami.getModel().getSteps().getStep().get(origami.getModel().getSteps().getStep().size() - 1), width, height / 2, withBackground); graphics.drawImage(image, bounds.x + insets.left, bounds.y + height / 2, null); label.paint(graphics.create(bounds.x + insets.left, bounds.y + (height / 2 - labelHeight) / 2, width, labelHeight)); try { if (progressCallback != null) progressCallback.run(); } catch (RuntimeException e) { throw new InterruptedException(); } } /** * Draw the given diagram page from the origami to the given graphics into the given rectangle. * * @param graphics The graphics to draw to. * @param bounds The rectangle to draw into. * @param insets insets of the page. * @param origami The origami to draw. * @param page The page to draw. * @param locale The locale used to generate step descriptions. * @param font The font to draw text with. * @param withBackground If true, fill background with diagram background color from the origami, else leave the * background transparent. * @param progressCallback The progress callback. */ protected void drawPage(Graphics2D graphics, Rectangle bounds, Insets insets, Origami origami, int page, Locale locale, Font font, boolean withBackground, Runnable progressCallback) throws InterruptedException { int cols = origami.getPaper().getCols() != null ? origami.getPaper().getCols() : 1; int rows = origami.getPaper().getRows() != null ? origami.getPaper().getRows() : 1; int width = (bounds.width - cols * insets.left - insets.right) / cols; int height = (bounds.height - rows * insets.top - insets.bottom) / rows; if (withBackground) graphics.setBackground(origami.getPaper().getBackgroundColor()); else graphics.setBackground(new Color(0, 0, 0, 0)); graphics.clearRect(0, 0, (int) bounds.getWidth(), (int) bounds.getHeight()); final Integer[] stepsPlacement = origami.getStepsPlacement(page); Step step = origami.getFirstStep(page); final int gridWidth = origami.getPaper().getCols(); StepThumbnailGenerator generator = ServiceLocator.get(StepThumbnailGenerator.class); JPanel panel = new JPanel(); panel.setSize(4000, 4000); JMultilineLabel label = new JMultilineLabel(""); label.setOpaque(false); label.setFont(font); panel.setVisible(true); label.setVisible(true); for (int i : stepsPlacement) { final int x = i % gridWidth; final int y = i / gridWidth; final Rectangle stepBounds = new Rectangle(insets.left + x * (width + insets.left), insets.top + y * (height + insets.top), width * (step.getColspan() != null ? step.getColspan() : 1), height * (step.getRowspan() != null ? step.getRowspan() : 1)); label.setText(step.getDescriptionWithId(locale)); int labelHeight; { // this block computes the label's height when we know its fixed maximum width panel.removeAll(); panel.setLayout(new FormLayout(stepBounds.width + "px", "default")); panel.add(label, new CellConstraints(1, 1)); Queue<Container> containers = new LinkedList<Container>(); containers.add(panel); Container container; while ((container = containers.poll()) != null) { container.doLayout(); for (Component c : container.getComponents()) { if (c instanceof Container) containers.add((Container) c); } } labelHeight = label.getSize().height; } Image image = generator.getThumbnail(origami, step, stepBounds.width, stepBounds.height - labelHeight, withBackground); graphics.drawImage(image, stepBounds.x, stepBounds.y, null); label.paint(graphics.create(stepBounds.x, stepBounds.y + stepBounds.height - labelHeight, stepBounds.width, labelHeight)); try { if (progressCallback != null) progressCallback.run(); } catch (RuntimeException e) { throw new InterruptedException(); } step = step.getNext(); } } @Override public Origami loadModel(final URI path, boolean onlyMetadata) throws IOException, UnsupportedDataFormatException { URL url = null; if (path.isAbsolute()) { url = path.toURL(); } else { url = new URL(documentBase, path.toString()); } return loadModel(url, onlyMetadata); } @Override public Origami loadModel(final URL path, boolean onlyMetadata) throws IOException, UnsupportedDataFormatException { try { BindingsManager manager = ServiceLocator.get(BindingsManager.class); BindingsController<Origami> controller = new BindingsController<Origami>(manager, LATEST_SCHEMA_NAMESPACE); InputStream input = path.openStream(); if (onlyMetadata) { TransformInfo transform = new TransformInfo(null, null, null, new MetadataLoadingHandler(input)); controller.addAdditionalTransform(transform, TransformLocation.BEFORE_UNMARSHALLER, true); } model = controller.unmarshal(new InputStreamReader(input, "UTF8")); if (onlyMetadata) { // if only metadata are loaded, we need to provide a method to lazily load the rest of the diagram model.setLoadModelCallable(new Callable<Model>() { @Override public Model call() throws Exception { BindingsManager manager = ServiceLocator.get(BindingsManager.class); BindingsController<Origami> controller = new BindingsController<Origami>(manager, LATEST_SCHEMA_NAMESPACE); return controller.unmarshal(new InputStreamReader(path.openStream())).getModel(); } }); } model.setSrc(path); return (Origami) new ObjectFactory().createOrigami(model).getValue(); } catch (UnmarshalException e) { throw new UnsupportedDataFormatException(e); } catch (JAXBException e) { throw new UnsupportedDataFormatException(e); } } /** * @return the documentBase */ public URL getDocumentBase() { return documentBase; } /** * @param documentBase the documentBase to set */ public void setDocumentBase(URL documentBase) { this.documentBase = documentBase; } /** * This XML filter first reads the version of the used schema, then decides, which JAXB unmarshaller it will use, * and, if desired, can stop reading when the model's metadata are read (in particular it will close the URL * connection immediately after the metadata are loaded). * * @author Martin Pecka */ protected class MetadataLoadingHandler extends ResultDelegatingDefaultHandler { /** * The input stream we are reading from. It is needed here in order to be able to prematurely close the * connection. */ protected InputStream is; /** The QName of the root element. */ protected String rootElementQName = null; /** * @param is The input stream we are reading from. * @param onlyMetadata If true, close the <code>is</code> when the <code>model</code> tag is encountered. */ protected MetadataLoadingHandler(InputStream is) { this.is = is; } @Override public void startElement(String namespaceURI, String localName, String qName, Attributes atts) throws SAXException { if (namespaceURI.equals(LATEST_SCHEMA_NAMESPACE) && localName.equals("origami")) { rootElementQName = qName; } else if (localName.equals("model")) { try { // metadate are loaded, so abort reading from the URL, close the connection is.close(); } catch (IOException e) {} super.startElement(namespaceURI, localName, qName, atts); // this will process the rest of the unmarshalling with the <model> tag empty endElement(namespaceURI, localName, qName); // this will end the marshalling at all endElement(LATEST_SCHEMA_NAMESPACE, "origami", rootElementQName); endDocument(); // abort parsing throw new ParseAbortedException(); } super.startElement(namespaceURI, localName, qName, atts); } } /** * A class that generates filenames for pages to be saved. * * @author Martin Pecka */ protected interface FileNameGenerator { /** * Return the filename of the given page. * * @param page The page number (starting from 1!). * @return The file name. */ String getFileName(int page); } /** * The default implementation of {@link FileNameGenerator} which includes the page number into the filename. * * @author Martin Pecka */ protected class FileNameGeneratorImpl implements FileNameGenerator { protected final String prefix; protected final String suffix; protected final int numPages; protected final int numDigits; /** * @param prefix The string to add before the digits. * @param suffix The string to add after the digits (including the dot and extension string). * @param numPages The overall number of pages this generator handles. */ public FileNameGeneratorImpl(String prefix, String suffix, int numPages) { this.prefix = prefix; this.suffix = suffix; this.numPages = numPages; this.numDigits = (int) (Math.floor(Math.log10(numPages)) + 1); } @Override public String getFileName(int page) { String digits = Integer.toString(page); if (digits.length() < numDigits) { StringBuilder zeros = new StringBuilder(); for (int i = 0; i < numDigits - digits.length(); i++) zeros.append("0"); digits = zeros + digits; } return prefix + digits + suffix; } } }