/* This file is part of docx-html-editor. docx-html-editor is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 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 Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.plutext.htmleditor; import java.awt.Color; import java.io.ByteArrayOutputStream; import java.io.StringWriter; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Logger; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import javax.xml.bind.Unmarshaller; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import org.capaxit.imagegenerator.Margin; import org.capaxit.imagegenerator.TextImage; import org.capaxit.imagegenerator.imagecallbacks.BackgroundColorCallback; import org.capaxit.imagegenerator.imageexporter.ImageType; import org.capaxit.imagegenerator.imageexporter.ImageWriter; import org.capaxit.imagegenerator.imageexporter.ImageWriterFactory; import org.capaxit.imagegenerator.impl.TextImageImpl; import org.capaxit.imagegenerator.textalign.GreedyTextWrapper; import org.docx4j.TextUtils; import org.docx4j.TraversalUtil; import org.docx4j.TraversalUtil.CallbackImpl; import org.docx4j.XmlUtils; import org.docx4j.convert.out.html.HTMLConversionContext; import org.docx4j.dml.CTBlip; import org.docx4j.dml.wordprocessingDrawing.Anchor; import org.docx4j.dml.wordprocessingDrawing.Inline; import org.docx4j.jaxb.Context; import org.docx4j.model.images.WordXmlPictureE10; import org.docx4j.model.images.WordXmlPictureE20; import org.docx4j.vml.CTImageData; import org.docx4j.wml.R; import org.w3c.dom.Document; import org.w3c.dom.DocumentFragment; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.traversal.NodeIterator; /** * Paragraph content (eg sectPr or oMathPara) can be preserved, * as can run content (most stuff is preserved at this level). * * Preserved structures are stored in a hashmap, and allocated * an ID. * * Reinstatement is then just a matter of matching the ID * on the non-editable content item image, with an entry in the * hashmap. */ public class LongTailHelper { private static final Logger jul = Logger.getLogger(LongTailHelper.class.getName()); private final static AtomicInteger counter = new AtomicInteger(); private static String getId() { return Editor.APP_KEY+"fcb"+counter.getAndIncrement(); // since we store this in the pkg user data, its nice to use a 'namespace' } /** * Takes a paragraph, or some specific object (eg a footnote reference), * and returns an image which will represent it for round trip purposes. * * @param context * @param nodeIt * @param objectCount * @param frozenContentTypeHint * @return */ public static DocumentFragment createFrozenContentBlock( HTMLConversionContext context, NodeIterator nodeIt, int objectCount, String frozenContentTypeHint, HttpServletResponse response) { /* self::v:textbox or self::v:imagedata or self::w:control or self::a:blip or self::dgm:relIds or self::w:pict or self::w:object or self::c:chart or self::m:oMathPara or self::w:sectPr or self::o:signatureline */ try { // context.getLog().info("objectCount " + objectCount); // jul.info("objectCount " + objectCount); Object jaxbEl = null; if (nodeIt!=null) { //It is never null Node n = nodeIt.nextNode(); if (n!=null) { Unmarshaller u = Context.jc.createUnmarshaller(); u.setEventHandler(new org.docx4j.jaxb.JaxbValidationEventHandler()); jaxbEl = u.unmarshal(n); } } // Store the frozen object in the pkg // (this is easier than subclassing HTMLSettings, passing the storage map through the XSLT etc) String id = getId(); context.getWmlPackage().setUserData(id, jaxbEl); // System.out.println(id + " --> \n" + XmlUtils.marshaltoString(jaxbEl, true, true)); // Get the text content of wmlP StringWriter sw = new StringWriter(); TextUtils.extractText(jaxbEl, sw); String textContent = sw.toString().trim(); if (objectCount==1) { if (frozenContentTypeHint.equals("sectPr")) { return staticPlaceholder("sectPr.png", id, true); } if (frozenContentTypeHint.equals("br")) { // TODO - handle other break types return staticPlaceholder("page_break.png", id, true); } if (frozenContentTypeHint.equals("tab")) { return staticPlaceholder("tab.png", id, false); } if (frozenContentTypeHint.equals("signatureline")) { return staticPlaceholder("signature.png", id, true); } if (frozenContentTypeHint.equals("chart")) { return staticPlaceholder("chart.png", id, true); } if (frozenContentTypeHint.equals("oMathPara")) { return staticPlaceholder("equation.png", id, true); } if (frozenContentTypeHint.equals("bookmarkStart")) { return staticPlaceholder("bookmark_start.png", id, false); } if (frozenContentTypeHint.equals("bookmarkEnd")) { return staticPlaceholder("bookmark_end.png", id, false); } if (frozenContentTypeHint.equals("commentRangeStart")) { return staticPlaceholder("comment_range_start.png", id, false); } if (frozenContentTypeHint.equals("commentRangeEnd")) { return staticPlaceholder("comment_range_end.png", id, false); } if (frozenContentTypeHint.equals("commentReference")) { return staticPlaceholder("comment_ref.png", id, false); } if (frozenContentTypeHint.equals("footnoteReference")) { return staticPlaceholder("footnote.png", id, false); } if (frozenContentTypeHint.equals("endnoteReference")) { return staticPlaceholder("endnote.png", id, false); } if (jaxbEl instanceof R) { R p = (R)jaxbEl; // Handle w:drawing image BlipFinder bf = new BlipFinder(); new TraversalUtil(p.getContent(), bf); SessionImageHandler sih = (SessionImageHandler)context.getImageHandler(); sih.setResponse(response); if (bf.blips.size()==1) { // handle an ordinary image - WordXmlPictureE20 DocumentFragment createHtmlImgE20 = WordXmlPictureE20.createHtmlImgE20(context, bf.anchorOrInline, id); return createHtmlImgE20; } else { // Handle w:pict//v:imagedata ShapeFinder sf = new ShapeFinder(); new TraversalUtil(p.getContent(), sf); if (sf.shapeRefs.size()==1) { // handle VML image DocumentFragment createHtmlImgE10 = WordXmlPictureE10.createHtmlImgE10(context, sf.pict, id); return createHtmlImgE10; } } } } // Otherwise, its not just a simple image if (context.getLog().isDebugEnabled() && jaxbEl!=null) { context.getLog().debug(XmlUtils.marshaltoString(jaxbEl, true, true)); } // this is where we generate a placeholder image HttpSession session = (HttpSession)context.getWmlPackage().getUserData("HttpSession"); String url = createPlaceholder(session, id, frozenContentTypeHint, textContent); // Create a DOM document to take the results DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); Document document = factory.newDocumentBuilder().newDocument(); // Element xhtmlBlock = document.createElement("p"); // document.appendChild(xhtmlBlock); Element xhtmlBlock = document.createElement("img"); document.appendChild(xhtmlBlock); xhtmlBlock.setAttribute("src", response.encodeURL(url)); xhtmlBlock.setAttribute("id", id); DocumentFragment docfrag = document.createDocumentFragment(); docfrag.appendChild(document.getDocumentElement()); return docfrag; } catch (Exception e) { context.getLog().error(e.getMessage(), e); } return null; } private static DocumentFragment staticPlaceholder(String imageName, String id, boolean block) { String url = Editor.getContextPath()+"/placeholders/" + imageName; // Create a DOM document to take the results DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); Document document=null; try { document = factory.newDocumentBuilder().newDocument(); } catch (ParserConfigurationException e) { // TODO Auto-generated catch block e.printStackTrace(); } // Element xhtmlBlock = document.createElement("p"); // document.appendChild(xhtmlBlock); Element xhtmlBlock = document.createElement("img"); document.appendChild(xhtmlBlock); xhtmlBlock.setAttribute("src", url); xhtmlBlock.setAttribute("id", id); if(block) { xhtmlBlock.setAttribute("style", "display:block"); } DocumentFragment docfrag = document.createDocumentFragment(); docfrag.appendChild(document.getDocumentElement()); return docfrag; } private static String createPlaceholder(HttpSession session, String id, String frozenContentTypeHint, String textContent) throws Exception { // There's another library https://code.google.com/p/litetext/ // which I haven't tried, developed for Google App Engine use where AWT and BufferedImage et al do not exist. TextImage textImage; if (frozenContentTypeHint.equals("textbox") ) { if (textContent.length()==0) { textContent="[empty text box]"; } int rows = (int) Math.ceil((double)textContent.length()/40); // 40 chars per row int height = rows * 16; textImage = new TextImageImpl(300, height, new Margin(0, 0)); textImage.useTextWrapper(new GreedyTextWrapper() ); textImage.wrap(true); textImage.performAction(new BackgroundColorCallback(Color.LIGHT_GRAY, Color.BLACK, textImage)); textImage.write(textContent); } else { // 1. create a new TextImageImpl with a size of 300x300 pixels // and a left and top margin of 5 pixels. The default font is SansSerif, // PLAIN,12 textImage = new TextImageImpl(100, 20, new Margin(0, 0)); // String tag = id.substring(id.indexOf(":")+1); // 2. These methods add text and a newline // textImage.writeLine("[" + tag + "]"); textImage.writeLine("[" + frozenContentTypeHint + "]"); } // 3. Add explicit newlines. All methods can be chained for convenience. // textImage.newLine().newLine(); // textImage.withFontStyle(Style.UNDERLINED).write("Hello world!"); ImageWriter imageWriter = ImageWriterFactory.getImageWriter(ImageType.PNG); ByteArrayOutputStream baos = new ByteArrayOutputStream(); // imageWriter.writeImageToFile(textImage, new File("simple.png")); imageWriter.writeImageToOutputStream(textImage, baos); // session.setAttribute(id, baos.toByteArray()); SessionImageHandler.getImageMap(session).put(id, baos.toByteArray()); return Editor.getContextPath() + "/services/image/" + id; } static class BlipFinder extends CallbackImpl { Object anchorOrInline; // BlipFinder is only useful to us when there is a single element in the List, so this is OK List<CTBlip> blips = new ArrayList<CTBlip>(); @Override public List<Object> apply(Object o) { if (o instanceof Inline) { anchorOrInline = o; } else if (o instanceof Anchor) { anchorOrInline = o; } else if (o instanceof CTBlip) { blips.add((CTBlip)o); } return null; } } static class ShapeFinder extends CallbackImpl { org.docx4j.wml.Pict pict; // ShapeFinder is only useful to us when there is a single element in the List, so this is OK List<CTImageData> shapeRefs = new ArrayList<CTImageData>(); @Override public List<Object> apply(Object o) { if (o instanceof org.docx4j.wml.Pict) { this.pict = (org.docx4j.wml.Pict)o; } else if (o instanceof org.docx4j.vml.CTImageData) { CTImageData imageData = (org.docx4j.vml.CTImageData)o; shapeRefs.add(imageData); } return null; } } // public static void main(String[] args) throws Exception { // String textContent = "Product Launch Revenue Plan"; // System.out.println(Math.ceil((double)textContent.length()/40)); // } }