diff --git a/html2image/pom.xml b/html2image/pom.xml index 5cb9118..f00bfc3 100644 --- a/html2image/pom.xml +++ b/html2image/pom.xml @@ -1,63 +1,111 @@ - + + + 4.0.0 + gui.ava html2image jar - 2.0-SNAPSHOT + 3.0.0-SNAPSHOT html2image - http://maven.apache.org + https://maven.apache.org + + + + 17 + UTF-8 + ${java.version} + ${java.version} + ${java.version} + 9.9.1 + + + - - org.xhtmlrenderer - core-renderer - R8 - + + + + net.sourceforge.nekohtml nekohtml - 1.9.14 + 1.9.22 + + + + + + + + + + + + + + + + + + + + org.xhtmlrenderer + flying-saucer-core + ${flying-saucer.version} - commons-lang - commons-lang - 2.5 + org.xhtmlrenderer + flying-saucer-pdf + ${flying-saucer.version} + + + - junit - junit - 4.8.1 - test + org.apache.commons + commons-lang3 + 3.16.0 + + + - org.springframework - spring-core - 3.0.3.RELEASE + org.junit.jupiter + junit-jupiter-engine + 5.11.0 test + + jfrog-third-party-releases-local - http://repo.jfrog.org/artifactory/third-party-releases-local + https://repo.jfrog.org/artifactory/third-party-releases-local + + diff --git a/html2image/src/main/java/gui/ava/html/Html2Image.java b/html2image/src/main/java/gui/ava/html/Html2Image.java index c8a7505..b5c47c9 100644 --- a/html2image/src/main/java/gui/ava/html/Html2Image.java +++ b/html2image/src/main/java/gui/ava/html/Html2Image.java @@ -1,5 +1,13 @@ package gui.ava.html; +import java.io.File; +import java.io.InputStream; +import java.io.Reader; +import java.net.URI; +import java.net.URL; + +import org.w3c.dom.Document; + import gui.ava.html.imagemap.HtmlImageMap; import gui.ava.html.imagemap.HtmlImageMapImpl; import gui.ava.html.parser.HtmlParser; @@ -8,19 +16,14 @@ import gui.ava.html.pdf.PdfRendererImpl; import gui.ava.html.renderer.ImageRenderer; import gui.ava.html.renderer.ImageRendererImpl; -import org.w3c.dom.Document; - -import java.io.File; -import java.io.InputStream; -import java.io.Reader; -import java.net.URI; -import java.net.URL; /** * @author Yoav Aharoni */ public class Html2Image { - private HtmlParser parser = new HtmlParserImpl(); + + private final HtmlParser parser = new HtmlParserImpl(); + private HtmlImageMap htmlImageMap; private ImageRenderer imageRenderer; private PdfRenderer pdfRenderer; @@ -91,4 +94,5 @@ public static Html2Image fromInputStream(InputStream inputStream) { html2Image.getParser().load(inputStream); return html2Image; } + } diff --git a/html2image/src/main/java/gui/ava/html/exception/RenderException.java b/html2image/src/main/java/gui/ava/html/exception/RenderException.java index 395a0ea..2c499ce 100644 --- a/html2image/src/main/java/gui/ava/html/exception/RenderException.java +++ b/html2image/src/main/java/gui/ava/html/exception/RenderException.java @@ -4,7 +4,9 @@ * @author Yoav Aharoni */ public class RenderException extends RuntimeException { + public RenderException(String message, Throwable cause) { super(message, cause); } + } diff --git a/html2image/src/main/java/gui/ava/html/image/HtmlImageGenerator.java b/html2image/src/main/java/gui/ava/html/image/HtmlImageGenerator.java index aaebabb..9620966 100644 --- a/html2image/src/main/java/gui/ava/html/image/HtmlImageGenerator.java +++ b/html2image/src/main/java/gui/ava/html/image/HtmlImageGenerator.java @@ -1,29 +1,29 @@ package gui.ava.html.image; -import gui.ava.html.image.util.FormatNameUtil; -import gui.ava.html.image.util.SynchronousHTMLEditorKit; -import gui.ava.html.link.LinkInfo; - -import javax.imageio.ImageIO; -import javax.swing.*; import java.awt.*; +import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; -import java.beans.PropertyChangeEvent; -import java.beans.PropertyChangeListener; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.net.URL; -import java.util.Collection; import java.util.Collections; import java.util.List; +import javax.imageio.ImageIO; +import javax.swing.*; + +import gui.ava.html.link.LinkInfo; +import gui.ava.html.util.FormatNameUtil; + /** * @author Yoav Aharoni */ public class HtmlImageGenerator { - private JEditorPane editorPane; - static final Dimension DEFAULT_SIZE = new Dimension(800, 800); + + private static final Dimension DEFAULT_SIZE = new Dimension(800, 800); + + private final JEditorPane editorPane; public HtmlImageGenerator() { editorPane = createJEditorPane(); @@ -63,8 +63,8 @@ public void loadUrl(String url) { public void loadHtml(String html) { editorPane.setEditable(false); - editorPane.setText(html); editorPane.setContentType("text/html"); + editorPane.setText(html); onDocumentLoad(); } @@ -72,15 +72,15 @@ public String getLinksMapMarkup(String mapName) { final StringBuilder markup = new StringBuilder(); markup.append("\n"); for (LinkInfo link : getLinks()) { - final List bounds = link.getBounds(); - for (Rectangle bound : bounds) { + final List bounds = link.getBounds(); + for (Rectangle2D bound : bounds) { final int x1 = (int) bound.getX(); final int y1 = (int) bound.getY(); final int x2 = (int) (x1 + bound.getWidth()); final int y2 = (int) (y1 + bound.getHeight()); markup.append(String.format("\n"); @@ -101,9 +101,7 @@ public void saveAsHtmlWithMap(String file, String imageUrl) { } public void saveAsHtmlWithMap(File file, String imageUrl) { - FileWriter writer = null; - try { - writer = new FileWriter(file); + try (FileWriter writer = new FileWriter(file)) { writer.append("\n"); writer.append("\n\n"); writer.append("\n"); @@ -115,15 +113,7 @@ public void saveAsHtmlWithMap(File file, String imageUrl) { writer.append("\n"); } catch (IOException e) { throw new RuntimeException(String.format("Exception while saving '%s' html file", file), e); - } finally { - if (writer != null) { - try { - writer.close(); - } catch (IOException ignore) { - } - } } - } public void saveAsImage(String file) { @@ -146,8 +136,7 @@ public void saveAsImage(File file) { } } - protected void onDocumentLoad() { - } + protected void onDocumentLoad() {} public Dimension getDefaultSize() { return DEFAULT_SIZE; @@ -179,25 +168,35 @@ protected JEditorPane createJEditorPane() { final SynchronousHTMLEditorKit kit = new SynchronousHTMLEditorKit(); editorPane.setEditorKitForContentType("text/html", kit); editorPane.setContentType("text/html"); - editorPane.addPropertyChangeListener(new PropertyChangeListener() { - public void propertyChange(PropertyChangeEvent evt) { - if (evt.getPropertyName().equals("page")) { - onDocumentLoad(); - } + editorPane.addPropertyChangeListener(event -> { + if (event.getPropertyName().equals("page")) { + onDocumentLoad(); } }); return editorPane; } public void show() { - JFrame.setDefaultLookAndFeelDecorated(true); - JFrame frame = new JFrame(); - frame.setTitle("My First Swing Application"); - frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); - JLabel label = new JLabel("Welcome"); - frame.add(label); - frame.add(editorPane); - frame.pack(); - frame.setVisible(true); + // the main window + final JFrame view = new JFrame(); + + // create the view + view.setTitle("HtmlImageGenerator"); + view.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + JLabel label = new JLabel("Label"); + view.add(label); + view.add(editorPane); + view.pack(); + + // set the system look & feel + try { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + SwingUtilities.updateComponentTreeUI(view); + } catch (Exception ignored) {} + + // show the view + view.setLocationByPlatform(true); + view.setVisible(true); } + } diff --git a/html2image/src/main/java/gui/ava/html/image/SynchronousHTMLEditorKit.java b/html2image/src/main/java/gui/ava/html/image/SynchronousHTMLEditorKit.java new file mode 100644 index 0000000..bf76118 --- /dev/null +++ b/html2image/src/main/java/gui/ava/html/image/SynchronousHTMLEditorKit.java @@ -0,0 +1,37 @@ +package gui.ava.html.image; + +import javax.swing.text.Document; +import javax.swing.text.Element; +import javax.swing.text.View; +import javax.swing.text.ViewFactory; +import javax.swing.text.html.HTMLDocument; +import javax.swing.text.html.HTMLEditorKit; +import javax.swing.text.html.ImageView; + +/** + * @author Yoav Aharoni + */ +public class SynchronousHTMLEditorKit extends HTMLEditorKit { + + @Override + public Document createDefaultDocument() { + HTMLDocument doc = (HTMLDocument) super.createDefaultDocument(); + doc.setAsynchronousLoadPriority(-1); + return doc; + } + + @Override + public ViewFactory getViewFactory() { + return new HTMLFactory() { + @Override + public View create(Element elem) { + View view = super.create(elem); + if (view instanceof ImageView imageView) { + imageView.setLoadsSynchronously(true); + } + return view; + } + }; + } + +} diff --git a/html2image/src/main/java/gui/ava/html/image/util/FormatNameUtil.java b/html2image/src/main/java/gui/ava/html/image/util/FormatNameUtil.java deleted file mode 100644 index ca644a5..0000000 --- a/html2image/src/main/java/gui/ava/html/image/util/FormatNameUtil.java +++ /dev/null @@ -1,37 +0,0 @@ - -package gui.ava.html.image.util; - -import java.util.HashMap; -import java.util.Map; - -/** - * @author Yoav Aharoni - */ -public class FormatNameUtil { - public static Map types = new HashMap(); - private static final String DEFAULT_FORMAT = "png"; - - static { - types.put("gif", "gif"); - types.put("jpg", "jpg"); - types.put("jpeg", "jpg"); - types.put("png", "png"); - } - - public static String formatForExtension(String extension) { - final String type = types.get(extension); - if (type == null) { - return DEFAULT_FORMAT; - } - return type; - } - - public static String formatForFilename(String fileName) { - final int dotIndex = fileName.lastIndexOf('.'); - if (dotIndex < 0) { - return DEFAULT_FORMAT; - } - final String ext = fileName.substring(dotIndex + 1); - return formatForExtension(ext); - } -} diff --git a/html2image/src/main/java/gui/ava/html/image/util/SynchronousHTMLEditorKit.java b/html2image/src/main/java/gui/ava/html/image/util/SynchronousHTMLEditorKit.java deleted file mode 100644 index f9f96f9..0000000 --- a/html2image/src/main/java/gui/ava/html/image/util/SynchronousHTMLEditorKit.java +++ /dev/null @@ -1,33 +0,0 @@ -package gui.ava.html.image.util; - -import javax.swing.text.Document; -import javax.swing.text.Element; -import javax.swing.text.View; -import javax.swing.text.ViewFactory; -import javax.swing.text.html.HTMLDocument; -import javax.swing.text.html.HTMLEditorKit; -import javax.swing.text.html.ImageView; - -/** - * @author Yoav Aharoni - */ -public class SynchronousHTMLEditorKit extends HTMLEditorKit { - - public Document createDefaultDocument() { - HTMLDocument doc = (HTMLDocument) super.createDefaultDocument(); - doc.setAsynchronousLoadPriority(-1); - return doc; - } - - public ViewFactory getViewFactory() { - return new HTMLFactory() { - public View create(Element elem) { - View view = super.create(elem); - if (view instanceof ImageView) { - ((ImageView) view).setLoadsSynchronously(true); - } - return view; - } - }; - } -} diff --git a/html2image/src/main/java/gui/ava/html/imagemap/ElementBox.java b/html2image/src/main/java/gui/ava/html/imagemap/ElementBox.java index 094ae9a..01f2655 100644 --- a/html2image/src/main/java/gui/ava/html/imagemap/ElementBox.java +++ b/html2image/src/main/java/gui/ava/html/imagemap/ElementBox.java @@ -1,18 +1,19 @@ package gui.ava.html.imagemap; -import org.w3c.dom.Element; - import java.util.Collection; +import org.w3c.dom.Element; + /** * @author Yoav Aharoni */ public class ElementBox { - private Element element; - private int left; - private int top; - private int width; - private int height; + + private final Element element; + private final int left; + private final int top; + private final int width; + private final int height; public ElementBox(Element element, int left, int top, int width, int height) { this.element = element; @@ -54,17 +55,22 @@ public boolean isEmpty() { return width <= 0 || height <= 0; } + public boolean containedIn(ElementBox box) { + return containedIn(this, box); + } + public boolean containedIn(Collection elementBoxes) { - for (ElementBox box : elementBoxes) { - if (containedIn(box)) { - return true; - } - } - return false; + return elementBoxes.stream().anyMatch(this::containedIn); } - public boolean containedIn(ElementBox box) { - return getTop() >= box.getTop() && getLeft() >= box.getTop() - && getBottom() <= box.getBottom() && getRight() <= box.getRight(); + /** + * @param box box to test + * @param other the other {@link ElementBox} + * @return true if box is contained inside the other {@link ElementBox} + */ + public static boolean containedIn(ElementBox box, ElementBox other) { + return box.getTop() >= other.getTop() && box.getLeft() >= other.getTop() + && box.getBottom() <= other.getBottom() && box.getRight() <= other.getRight(); } + } diff --git a/html2image/src/main/java/gui/ava/html/imagemap/HtmlImageMap.java b/html2image/src/main/java/gui/ava/html/imagemap/HtmlImageMap.java index 6b38f89..50b2047 100644 --- a/html2image/src/main/java/gui/ava/html/imagemap/HtmlImageMap.java +++ b/html2image/src/main/java/gui/ava/html/imagemap/HtmlImageMap.java @@ -1,16 +1,17 @@ package gui.ava.html.imagemap; -import org.w3c.dom.Element; - import java.io.File; import java.io.Writer; import java.util.Collection; import java.util.Map; +import org.w3c.dom.Element; + /** * @author Yoav Aharoni */ public interface HtmlImageMap { + Map> getClickableBoxes(); String getImageMap(String mapName, String imageURL); @@ -24,4 +25,5 @@ public interface HtmlImageMap { void saveImageMapDocument(File file, String imageURL); void saveImageMapDocument(Writer writer, String imageURL, boolean closeWriter); + } diff --git a/html2image/src/main/java/gui/ava/html/imagemap/HtmlImageMapImpl.java b/html2image/src/main/java/gui/ava/html/imagemap/HtmlImageMapImpl.java index ae7ece9..c1de04b 100644 --- a/html2image/src/main/java/gui/ava/html/imagemap/HtmlImageMapImpl.java +++ b/html2image/src/main/java/gui/ava/html/imagemap/HtmlImageMapImpl.java @@ -1,34 +1,46 @@ package gui.ava.html.imagemap; -import gui.ava.html.exception.RenderException; -import gui.ava.html.renderer.LayoutHolder; -import org.apache.commons.lang.StringUtils; +import static java.lang.String.format; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.StringWriter; +import java.io.Writer; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; + +import gui.ava.html.exception.RenderException; +import gui.ava.html.renderer.LayoutHolder; import org.xhtmlrenderer.layout.Styleable; import org.xhtmlrenderer.render.BlockBox; import org.xhtmlrenderer.render.Box; import org.xhtmlrenderer.render.InlineLayoutBox; import org.xhtmlrenderer.render.LineBox; -import java.io.*; -import java.util.*; - -import static java.lang.String.format; - /** * @author Yoav Aharoni */ public class HtmlImageMapImpl implements HtmlImageMap { - private static Set searchedAttributes = stringSet("href", "onclick", "ondblclick", "onmousedown", "onmouseup"); - private static Set allowedAttributes = stringSet( + + private static final Set searchedAttributes = Set.of("href", "onclick", "ondblclick", "onmousedown", "onmouseup"); + + private static final Set allowedAttributes = Set.of( "href", "target", "title", "class", "tabindex", "dir", "lang", "accesskey", "onblur", "onclick", "ondblclick", "onfocus", "onmousedown", "onmousemove", "onmouseout", "onmouseover", "onmouseup", "onkeydown", "onkeypress", "onkeyup"); - private LayoutHolder layoutHolder; + private final LayoutHolder layoutHolder; public HtmlImageMapImpl(LayoutHolder layoutHolder) { this.layoutHolder = layoutHolder; @@ -120,12 +132,11 @@ public void saveImageMapDocument(String filename, String imageURL) { @Override public Map> getClickableBoxes() { final Box rootBox = layoutHolder.getRootBox(); - final HashMap> boxes = new HashMap>(); - addClickableElements(rootBox, boxes, new HashSet()); + final HashMap> boxes = new HashMap<>(); + addClickableElements(rootBox, boxes, new HashSet<>()); return boxes; } - @SuppressWarnings({"unchecked"}) private void addClickableElements(Styleable styleable, HashMap> boxes, Set visited) { if (styleable == null || visited.contains(styleable)) { return; @@ -134,26 +145,26 @@ private void addClickableElements(Styleable styleable, HashMap) ((Box) styleable).getChildren()) { + if (styleable instanceof Box box) { + for (Styleable child : box.getChildren()) { addClickableElements(child, boxes, visited); } } - if (styleable instanceof InlineLayoutBox) { - for (Object child : (List) ((InlineLayoutBox) styleable).getInlineChildren()) { - if (child instanceof Styleable) { - addClickableElements((Styleable) child, boxes, visited); + if (styleable instanceof InlineLayoutBox inlineLayoutBox) { + for (Object child : inlineLayoutBox.getInlineChildren()) { + if (child instanceof Styleable styleableChild) { + addClickableElements(styleableChild, boxes, visited); } } - } else if (styleable instanceof BlockBox) { - final List content = (List) ((BlockBox) styleable).getInlineContent(); + } else if (styleable instanceof BlockBox blockBox) { + final List content = blockBox.getInlineContent(); if (content != null) { for (Styleable child : content) { addClickableElements(child, boxes, visited); } } - } else if (styleable instanceof LineBox) { - for (Styleable child : (List) ((LineBox) styleable).getNonFlowContent()) { + } else if (styleable instanceof LineBox lineBox) { + for (Styleable child : lineBox.getNonFlowContent()) { addClickableElements(child, boxes, visited); } } @@ -170,7 +181,7 @@ private void addIfClickable(Styleable styleable, HashMap elementBoxes = boxes.get(clickable); if (elementBoxes == null) { - elementBoxes = new ArrayList(); + elementBoxes = new ArrayList<>(); boxes.put(clickable, elementBoxes); elementBoxes.add(elementBox); return; @@ -181,14 +192,12 @@ private void addIfClickable(Styleable styleable, HashMap stringSet(String... items) { - return new HashSet(Arrays.asList(items)); - } } diff --git a/html2image/src/main/java/gui/ava/html/link/LinkHarvester b/html2image/src/main/java/gui/ava/html/link/LinkHarvester deleted file mode 100644 index 0323f3b..0000000 --- a/html2image/src/main/java/gui/ava/html/link/LinkHarvester +++ /dev/null @@ -1,77 +0,0 @@ -package gui.ava.html.link; - -import javax.swing.*; -import javax.swing.text.*; -import javax.swing.text.html.HTML; -import java.awt.*; -import java.util.ArrayList; -import java.util.Enumeration; -import java.util.List; - -/** - * @author Yoav Aharoni - */ -public class LinkHarvester { - private final JTextComponent textComponent; - private final List links = new ArrayList(); - - public LinkHarvester(JEditorPane textComponent) { - this.textComponent = textComponent; - harvestElement(textComponent.getDocument().getDefaultRootElement()); - } - - public List getLinks() { - return links; - } - - private void harvestElement(Element element) { - if (element == null) { - return; - } - - final AttributeSet attributes = element.getAttributes(); - final Enumeration attributeNames = attributes.getAttributeNames(); - while (attributeNames.hasMoreElements()) { - final Object key = attributeNames.nextElement(); - if (HTML.Tag.A.equals(key)) { - final Object value = attributes.getAttribute(key); - if (value instanceof SimpleAttributeSet) { - final SimpleAttributeSet attributeSet = (SimpleAttributeSet) value; - final String href = (String) attributeSet.getAttribute(HTML.Attribute.HREF); - final String title = (String) attributeSet.getAttribute(HTML.Attribute.TITLE); - final List bounds = elementBounds(element); - links.add(new LinkInfo(href, title, bounds)); - } - } - } - - for (int i = 0; i < element.getElementCount(); i++) { - final Element child = element.getElement(i); - harvestElement(child); - } - } - - private List elementBounds(Element element) { - final List bounds = new ArrayList(); - try { - final int startOffset = element.getStartOffset(); - final int endOffset = element.getEndOffset(); - Rectangle rectangle = textComponent.modelToView(startOffset); - for (int i = startOffset + 1; i <= endOffset; i++) { - final Rectangle temp = textComponent.modelToView(i); - if (temp.getY() == rectangle.getY()) { - rectangle = rectangle.union(temp); - } else { - bounds.add(rectangle); - rectangle = null; - } - } - if (rectangle != null) { - bounds.add(rectangle); - } - return bounds; - } catch (BadLocationException e) { - throw new RuntimeException("Got BadLocationException", e); - } - } -} diff --git a/html2image/src/main/java/gui/ava/html/link/LinkHarvester.java b/html2image/src/main/java/gui/ava/html/link/LinkHarvester.java new file mode 100644 index 0000000..7d65ba9 --- /dev/null +++ b/html2image/src/main/java/gui/ava/html/link/LinkHarvester.java @@ -0,0 +1,83 @@ +package gui.ava.html.link; + +import java.awt.geom.Rectangle2D; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; + +import javax.swing.*; +import javax.swing.text.AttributeSet; +import javax.swing.text.BadLocationException; +import javax.swing.text.Element; +import javax.swing.text.JTextComponent; +import javax.swing.text.SimpleAttributeSet; +import javax.swing.text.html.HTML; + +/** + * @author Yoav Aharoni + */ +public class LinkHarvester { + + private final JTextComponent textComponent; + private final List links = new ArrayList<>(); + + public LinkHarvester(JEditorPane textComponent) { + this.textComponent = textComponent; + harvestElement(textComponent.getDocument().getDefaultRootElement()); + } + + public List getLinks() { + return links; + } + + private void harvestElement(Element element) { + if (element == null) { + return; + } + + final AttributeSet attributes = element.getAttributes(); + final Enumeration attributeNames = attributes.getAttributeNames(); + while (attributeNames.hasMoreElements()) { + final Object key = attributeNames.nextElement(); + if (HTML.Tag.A.equals(key)) { + final Object value = attributes.getAttribute(key); + if (value instanceof SimpleAttributeSet attributeSet) { + final String href = (String) attributeSet.getAttribute(HTML.Attribute.HREF); + final String title = (String) attributeSet.getAttribute(HTML.Attribute.TITLE); + final List bounds = elementBounds(element); + links.add(new LinkInfo(href, title, bounds)); + } + } + } + + for (int i = 0; i < element.getElementCount(); i++) { + final Element child = element.getElement(i); + harvestElement(child); + } + } + + private List elementBounds(Element element) { + final List bounds = new ArrayList<>(); + try { + final int startOffset = element.getStartOffset(); + final int endOffset = element.getEndOffset(); + Rectangle2D rectangle = textComponent.modelToView2D(startOffset); + for (int i = startOffset + 1; i <= endOffset; i++) { + final Rectangle2D temp = textComponent.modelToView2D(i); + if (temp.getY() == rectangle.getY()) { + Rectangle2D.union(rectangle, temp, rectangle); + } else { + bounds.add(rectangle); + rectangle = null; + } + } + if (rectangle != null) { + bounds.add(rectangle); + } + return bounds; + } catch (BadLocationException e) { + throw new RuntimeException("Got BadLocationException", e); + } + } + +} diff --git a/html2image/src/main/java/gui/ava/html/link/LinkInfo.java b/html2image/src/main/java/gui/ava/html/link/LinkInfo.java index abbbb36..72aeed1 100644 --- a/html2image/src/main/java/gui/ava/html/link/LinkInfo.java +++ b/html2image/src/main/java/gui/ava/html/link/LinkInfo.java @@ -1,31 +1,33 @@ package gui.ava.html.link; -import java.awt.*; +import java.awt.geom.Rectangle2D; import java.util.List; /** * @author Yoav Aharoni */ public class LinkInfo { - private String href; - private String title; - private List bounds; - - public LinkInfo(String href, String title, List bounds) { - this.href = href; - this.title = title; - this.bounds = bounds; - } - - public String getHref() { - return href; - } - - public String getTitle() { - return title; - } - - public List getBounds() { - return bounds; - } + + private final String href; + private final String title; + private final List bounds; + + public LinkInfo(String href, String title, List bounds) { + this.href = href; + this.title = title; + this.bounds = bounds; + } + + public String getHref() { + return href; + } + + public String getTitle() { + return title; + } + + public List getBounds() { + return bounds; + } + } diff --git a/html2image/src/main/java/gui/ava/html/parser/HtmlParser.java b/html2image/src/main/java/gui/ava/html/parser/HtmlParser.java index 6066793..c929245 100644 --- a/html2image/src/main/java/gui/ava/html/parser/HtmlParser.java +++ b/html2image/src/main/java/gui/ava/html/parser/HtmlParser.java @@ -1,14 +1,15 @@ package gui.ava.html.parser; -import org.apache.xerces.parsers.DOMParser; -import org.w3c.dom.Document; - import java.io.File; import java.io.InputStream; import java.io.Reader; import java.net.URI; import java.net.URL; +import org.w3c.dom.Document; + +import org.apache.xerces.parsers.DOMParser; + /** * @author Yoav Aharoni */ @@ -33,4 +34,5 @@ public interface HtmlParser extends DocumentHolder { void loadHtml(String html); void loadURI(String uri); + } diff --git a/html2image/src/main/java/gui/ava/html/parser/HtmlParserImpl.java b/html2image/src/main/java/gui/ava/html/parser/HtmlParserImpl.java index 2c57e60..13c2102 100644 --- a/html2image/src/main/java/gui/ava/html/parser/HtmlParserImpl.java +++ b/html2image/src/main/java/gui/ava/html/parser/HtmlParserImpl.java @@ -1,36 +1,42 @@ package gui.ava.html.parser; -import org.apache.xerces.parsers.DOMParser; -import org.cyberneko.html.HTMLConfiguration; +import static java.lang.String.format; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.io.StringReader; +import java.net.URI; +import java.net.URL; + import org.w3c.dom.Document; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.SAXNotRecognizedException; import org.xml.sax.SAXNotSupportedException; -import java.io.*; -import java.net.URI; -import java.net.URL; - -import static java.lang.String.format; +import org.apache.xerces.parsers.DOMParser; +import org.cyberneko.html.HTMLConfiguration; /** * @author Yoav Aharoni */ public class HtmlParserImpl implements HtmlParser { + private DOMParser domParser; private Document document; public HtmlParserImpl() { - domParser = new DOMParser(new HTMLConfiguration()); try { + domParser = new DOMParser(new HTMLConfiguration()); // HtmlUnit 1.9.x + // domParser = new DOMParser(HTMLDocumentImpl.class); // HtmlUnit 4.x domParser.setProperty("http://cyberneko.org/html/properties/names/elems", "lower"); - } catch (SAXNotRecognizedException e) { - throw new ParseException("Can't create HtmlParserImpl", e); - } catch (SAXNotSupportedException e) { + } + catch (SAXNotRecognizedException | SAXNotSupportedException e) { throw new ParseException("Can't create HtmlParserImpl", e); } - } + } @Override public DOMParser getDomParser() { @@ -54,37 +60,26 @@ public void setDocument(Document document) { @Override public void load(Reader reader) { - try { - domParser.parse(new InputSource(reader)); - document = domParser.getDocument(); - } catch (SAXException e) { - throw new ParseException("SAXException while parsing HTML.", e); - } catch (IOException e) { - throw new ParseException("IOException while parsing HTML.", e); - } finally { - try { - reader.close(); - } catch (IOException ignore) { - } - } + try (reader) { + domParser.parse(new InputSource(reader)); + document = domParser.getDocument(); + } catch (SAXException e) { + throw new ParseException("SAXException while parsing HTML.", e); + } catch (IOException e) { + throw new ParseException("IOException while parsing HTML.", e); + } } @Override public void load(InputStream inputStream) { - try { - domParser.parse(new InputSource(inputStream)); - document = domParser.getDocument(); - } catch (SAXException e) { - throw new ParseException("SAXException while parsing HTML.", e); - } catch (IOException e) { - throw new ParseException("IOException while parsing HTML.", e); - } - finally { - try { - inputStream.close(); - } catch (IOException ignore) { - } - } + try (inputStream) { + domParser.parse(new InputSource(inputStream)); + document = domParser.getDocument(); + } catch (SAXException e) { + throw new ParseException("SAXException while parsing HTML.", e); + } catch (IOException e) { + throw new ParseException("IOException while parsing HTML.", e); + } } @Override @@ -92,12 +87,10 @@ public void loadURI(String uri) { try { domParser.parse(new InputSource(uri)); document = domParser.getDocument(); - } catch (SAXException e) { - throw new ParseException(format("SAXException while parsing HTML from \"%s\".", uri), e); - } catch (IOException e) { + } catch (SAXException | IOException e) { throw new ParseException(format("SAXException while parsing HTML from \"%s\".", uri), e); } - } + } @Override public void load(File file) { @@ -118,4 +111,5 @@ public void load(URI uri) { public void loadHtml(String html) { load(new StringReader(html)); } + } diff --git a/html2image/src/main/java/gui/ava/html/pdf/PdfRendererImpl.java b/html2image/src/main/java/gui/ava/html/pdf/PdfRendererImpl.java index 94221e2..38b01cb 100644 --- a/html2image/src/main/java/gui/ava/html/pdf/PdfRendererImpl.java +++ b/html2image/src/main/java/gui/ava/html/pdf/PdfRendererImpl.java @@ -1,18 +1,24 @@ package gui.ava.html.pdf; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +import org.w3c.dom.Document; + import com.lowagie.text.DocumentException; import gui.ava.html.exception.RenderException; import gui.ava.html.parser.DocumentHolder; -import org.w3c.dom.Document; import org.xhtmlrenderer.pdf.ITextRenderer; -import java.io.*; - /** * @author Yoav Aharoni */ public class PdfRendererImpl implements PdfRenderer { - private DocumentHolder documentHolder; + + private final DocumentHolder documentHolder; public PdfRendererImpl(DocumentHolder documentHolder) { this.documentHolder = documentHolder; @@ -51,4 +57,5 @@ public void saveToPDF(File file) { public void saveToPDF(String file) { saveToPDF(new File(file)); } + } diff --git a/html2image/src/main/java/gui/ava/html/renderer/CustomizableFSImageWriter.java b/html2image/src/main/java/gui/ava/html/renderer/CustomizableFSImageWriter.java new file mode 100644 index 0000000..68ffbcc --- /dev/null +++ b/html2image/src/main/java/gui/ava/html/renderer/CustomizableFSImageWriter.java @@ -0,0 +1,93 @@ +package gui.ava.html.renderer; + +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Iterator; + +import javax.imageio.IIOImage; +import javax.imageio.ImageIO; +import javax.imageio.ImageWriteParam; +import javax.imageio.ImageWriter; +import javax.imageio.stream.ImageOutputStream; + +import org.xhtmlrenderer.util.FSImageWriter; + +/** + * Flying Saucer 9.x forgot to create a constructor to set the compression parameters! + */ +public class CustomizableFSImageWriter extends FSImageWriter { + + private final String imageFormat; + private final int writeCompressionMode; + private final float writeCompressionQuality; + private final String writeCompressionType; + + public CustomizableFSImageWriter(String imageFormat, int writeCompressionMode, float writeCompressionQuality, String writeCompressionType) { + super(imageFormat); + this.imageFormat = imageFormat; + this.writeCompressionMode = writeCompressionMode; + this.writeCompressionQuality = writeCompressionQuality; + this.writeCompressionType = writeCompressionType; + } + + /** + * Write the passed image to the passed OutputStream. + * Close the output stream only if the closeStream param is true. + */ + public void write(BufferedImage image, OutputStream os, boolean closeStream) throws IOException { + ImageWriter writer = lookupImageWriterForFormat(this.imageFormat); + + try { + ImageOutputStream ios = ImageIO.createImageOutputStream(os); + + try { + writer.setOutput(ios); + ImageWriteParam parameters = getImageWriteParameters(writer); + writer.write(null, new IIOImage(image, null, null), parameters); + ios.flush(); + } + catch (Throwable t) { + if (closeStream && ios != null) { + try { + ios.close(); + } catch (Throwable t2) { + t.addSuppressed(t2); + } + } + + throw t; + } + + if (closeStream && ios != null) { + ios.close(); + } + } finally { + writer.dispose(); + } + } + + @Override + protected ImageWriteParam getImageWriteParameters(ImageWriter writer) { + ImageWriteParam param = writer.getDefaultWriteParam(); + if (param.canWriteCompressed() && this.writeCompressionMode != 3) { + param.setCompressionMode(this.writeCompressionMode); + if (this.writeCompressionMode == 2) { + param.setCompressionType(this.writeCompressionType); + param.setCompressionQuality(this.writeCompressionQuality); + } + } + + return param; + } + + private static ImageWriter lookupImageWriterForFormat(String imageFormat) { + Iterator iter = ImageIO.getImageWritersByFormatName(imageFormat); + if (iter.hasNext()) { + return iter.next(); + } else { + throw new IllegalArgumentException("Image writer not found for format " + imageFormat); + } + } + +} diff --git a/html2image/src/main/java/gui/ava/html/renderer/FormatNameUtil.java b/html2image/src/main/java/gui/ava/html/renderer/FormatNameUtil.java deleted file mode 100644 index 6bd9d3b..0000000 --- a/html2image/src/main/java/gui/ava/html/renderer/FormatNameUtil.java +++ /dev/null @@ -1,41 +0,0 @@ -package gui.ava.html.renderer; - -import java.util.HashMap; -import java.util.Map; - -/** - * @author Yoav Aharoni - */ -public class FormatNameUtil { - public static Map types = new HashMap(); - private static final String DEFAULT_FORMAT = "png"; - - static { - types.put("gif", "gif"); - types.put("jpg", "jpg"); - types.put("jpeg", "jpg"); - types.put("png", "png"); - types.put("bmp", "bmp"); - } - - public static String formatForExtension(String extension) { - final String type = types.get(extension); - if (type == null) { - return DEFAULT_FORMAT; - } - return type; - } - - public static String getDefaultFormat() { - return DEFAULT_FORMAT; - } - - public static String formatForFilename(String fileName) { - final int dotIndex = fileName.lastIndexOf('.'); - if (dotIndex < 0) { - return DEFAULT_FORMAT; - } - final String ext = fileName.substring(dotIndex + 1); - return formatForExtension(ext); - } -} diff --git a/html2image/src/main/java/gui/ava/html/renderer/ImageRenderer.java b/html2image/src/main/java/gui/ava/html/renderer/ImageRenderer.java index e12ce31..7127f50 100644 --- a/html2image/src/main/java/gui/ava/html/renderer/ImageRenderer.java +++ b/html2image/src/main/java/gui/ava/html/renderer/ImageRenderer.java @@ -8,6 +8,7 @@ * @author Yoav Aharoni */ public interface ImageRenderer extends LayoutHolder { + int getWidth(); ImageRenderer setWidth(int width); diff --git a/html2image/src/main/java/gui/ava/html/renderer/ImageRendererImpl.java b/html2image/src/main/java/gui/ava/html/renderer/ImageRendererImpl.java index cc7c3ad..40b1fb1 100644 --- a/html2image/src/main/java/gui/ava/html/renderer/ImageRendererImpl.java +++ b/html2image/src/main/java/gui/ava/html/renderer/ImageRendererImpl.java @@ -1,25 +1,35 @@ package gui.ava.html.renderer; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Set; + +import javax.imageio.ImageWriteParam; + +import org.w3c.dom.Document; + import gui.ava.html.exception.RenderException; import gui.ava.html.parser.DocumentHolder; -import org.w3c.dom.Document; +import gui.ava.html.util.FormatNameUtil; import org.xhtmlrenderer.render.Box; import org.xhtmlrenderer.simple.Graphics2DRenderer; -import org.xhtmlrenderer.util.FSImageWriter; - -import javax.imageio.ImageWriteParam; -import java.awt.*; -import java.awt.image.BufferedImage; -import java.io.*; /** * @author Yoav Aharoni */ public class ImageRendererImpl implements ImageRenderer { + + private static final Set IMAGE_FORMAT_WITH_ALPHA = Set.of("gif","png"); + public static final int DEFAULT_WIDTH = 1024; public static final int DEFAULT_HEIGHT = 768; - private DocumentHolder documentHolder; + private final DocumentHolder documentHolder; private int width = DEFAULT_WIDTH; private int height = DEFAULT_HEIGHT; @@ -190,31 +200,30 @@ public void saveImage(String filename) { private void save(OutputStream outputStream, String filename, boolean closeStream) { try { - final String imageFormat = getImageFormat(filename); - final FSImageWriter imageWriter = getImageWriter(imageFormat); - final boolean isBMP = "bmp".equalsIgnoreCase(imageFormat); - final BufferedImage bufferedImage = getBufferedImage(isBMP ? BufferedImage.TYPE_INT_RGB : BufferedImage.TYPE_INT_ARGB); - imageWriter.write(bufferedImage, outputStream); - } catch (IOException e) { + final String imageFormat = FormatNameUtil.formatForFilename(filename); + final boolean hasAlpha = IMAGE_FORMAT_WITH_ALPHA.contains(imageFormat); + final BufferedImage bufferedImage = getBufferedImage(hasAlpha ? BufferedImage.TYPE_INT_ARGB : BufferedImage.TYPE_INT_RGB); + final CustomizableFSImageWriter imageWriter = getImageWriter(imageFormat); + imageWriter.write(bufferedImage, outputStream, closeStream); + } + catch (IOException e) { throw new RenderException("IOException while rendering image", e); - } finally { + } + finally { if (closeStream) { try { outputStream.close(); - } catch (IOException ignore) { } + catch (IOException ignore) {} } } } - private FSImageWriter getImageWriter(String imageFormat) { - FSImageWriter imageWriter = new FSImageWriter(imageFormat); - imageWriter.setWriteCompressionMode(writeCompressionMode); - imageWriter.setWriteCompressionQuality(writeCompressionQuality); - imageWriter.setWriteCompressionType(writeCompressionType); - return imageWriter; - } - + /** + * note: do not cache the format, otherwise multiple calls will return the same value! :) + * @deprecated use {@link FormatNameUtil#formatForFilename(String)} + */ + @Deprecated private String getImageFormat(String filename) { if (this.imageFormat != null) { return imageFormat; @@ -222,6 +231,11 @@ private String getImageFormat(String filename) { if (filename != null) { return FormatNameUtil.formatForFilename(filename); } - return FormatNameUtil.getDefaultFormat(); + return FormatNameUtil.DEFAULT_FORMAT; } + + private CustomizableFSImageWriter getImageWriter(String imageFormat) { + return new CustomizableFSImageWriter(imageFormat, writeCompressionMode, writeCompressionQuality, writeCompressionType); + } + } \ No newline at end of file diff --git a/html2image/src/main/java/gui/ava/html/util/FormatNameUtil.java b/html2image/src/main/java/gui/ava/html/util/FormatNameUtil.java new file mode 100644 index 0000000..cb1fe57 --- /dev/null +++ b/html2image/src/main/java/gui/ava/html/util/FormatNameUtil.java @@ -0,0 +1,41 @@ + +package gui.ava.html.util; + +import java.util.Map; +import java.util.Objects; + +/** + * @author Yoav Aharoni + */ +public enum FormatNameUtil { ; + + public static final String DEFAULT_FORMAT = "png"; + + public static final Map types = Map.of( + "gif", "gif", + "jpg", "jpg", + "jpeg", "jpg", + "png", "png", + "bmp", "bmp" + ); + + public static String formatForExtension(String extension) { + Objects.requireNonNull(extension); + final String type = types.get(extension); + if (type == null) { + return DEFAULT_FORMAT; + } + return type; + } + + public static String formatForFilename(String fileName) { + if (fileName == null) return DEFAULT_FORMAT; + final int dotIndex = fileName.lastIndexOf('.'); + if (dotIndex < 0) { + return DEFAULT_FORMAT; + } + final String ext = fileName.substring(dotIndex + 1); + return formatForExtension(ext); + } + +} diff --git a/html2image/src/test/java/gui/ava/html/BaseTest.java b/html2image/src/test/java/gui/ava/html/BaseTest.java index 8d7dcd4..4053951 100644 --- a/html2image/src/test/java/gui/ava/html/BaseTest.java +++ b/html2image/src/test/java/gui/ava/html/BaseTest.java @@ -1,21 +1,125 @@ package gui.ava.html; -import org.springframework.util.ResourceUtils; - +import java.io.File; import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.net.URISyntaxException; import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.logging.Logger; /** * @author Yoav Aharoni */ public class BaseTest { - public static final String TEST1_PATH = "classpath:test1.html"; + + private static final String PROJECT_NAME = "html2image"; + private static final String TEST_OUTPUT_PATH = "./output"; + + static { + try { + Files.createDirectories(getPath(TEST_OUTPUT_PATH)); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + public static File getTestOutputFile(String filename) { + return new File( getFile(TEST_OUTPUT_PATH), filename ); + } + + private static final String TEST1_PATH = "test1.html"; public static URL getTest1Url() { try { - return ResourceUtils.getURL(TEST1_PATH); + return getURL(TEST1_PATH); } catch (FileNotFoundException e) { throw new RuntimeException(e); } } + + public static URL getURL(String filename) throws FileNotFoundException { + URL url = Thread.currentThread().getContextClassLoader().getResource(filename); + if (url == null) throw new FileNotFoundException(filename+" not found"); + return url; + } + + public static File getTest1File() throws FileNotFoundException { + return getTestResourceFile(TEST1_PATH); + } + + public static File getTestResourceFile(String filename) throws FileNotFoundException { + try { + File input = getResourceFile(filename); + if (input.exists()) return input; + } + catch (URISyntaxException e) { + throw new IllegalArgumentException(e); + } + throw new FileNotFoundException(filename); + } + + + public static String getBasePath() { + try { + String sourcePath = getURL(".").getPath(); + String basePath = sourcePath.substring(0, sourcePath.lastIndexOf(PROJECT_NAME)-1); + return basePath; + } + catch (FileNotFoundException e) { + Logger.getLogger(BaseTest.class.getName()).warning("base path not found, fallback to 'user.dir' system property"); + return getUserProjectRootDirectory().toString(); + } + } + + public static Path getUserProjectRootDirectory() { + String envRootDir = System.getProperty("user.dir"); + Path rootDir = Paths.get(".").normalize().toAbsolutePath(); + if ( rootDir.startsWith(envRootDir) ) { + return rootDir; + } else { + throw new RuntimeException("Root dir not found in user directory."); + } + } + + + private static File getResourceFile(String filename) throws FileNotFoundException, URISyntaxException { + return new File(getURL(filename).toURI()); + } + + private static File getFile(String filename) { + return new File(getBasePath(), filename); + } + + private static Path getPath(String filename) { + return Path.of(getBasePath(), filename); + } + + + +// public static void main(String[] args) throws FileNotFoundException { +//// final File sourceFile = new File(BaseTest.class.getProtectionDomain().getCodeSource().getLocation().toString()); +//// System.out.println(sourceFile); +//// System.out.println(BaseTest.class.getClassLoader().getResource(TEST1_PATH)); +//// System.out.println(Thread.currentThread().getContextClassLoader().getResource(TEST1_PATH)); +// +//// final String packagePath = BaseTest.class.getPackageName().replace('.', '/'); +//// final String fullPath = BaseTest.class.getResource("./").toString(); +//// final String basePath = fullPath.substring(0, (fullPath.length()-packagePath.length()-1) ); +//// System.out.println(packagePath); +//// System.out.println(fullPath); +//// System.out.println(basePath); +// +//// String sourcePath = getURL(".").getPath(); +//// String basePath = sourcePath.substring(0, sourcePath.lastIndexOf(PROJECT_NAME)); +//// System.out.println(sourcePath); +//// System.out.println(basePath); +// +// System.out.println(getBasePath()); +// System.out.println(getUsersProjectRootDirectory()); +// } + } diff --git a/html2image/src/test/java/gui/ava/html/Example.java b/html2image/src/test/java/gui/ava/html/Example.java index 169a00a..d47ec76 100644 --- a/html2image/src/test/java/gui/ava/html/Example.java +++ b/html2image/src/test/java/gui/ava/html/Example.java @@ -1,17 +1,37 @@ package gui.ava.html; -import gui.ava.html.image.*; +import gui.ava.html.image.HtmlImageGenerator; /** * Created by hki on 07-01-2016. */ -public class Example { +public class Example extends BaseTest { + + private static final String html = """ + + + +