package org.expeditee.io; import java.awt.Color; import java.awt.Font; import java.awt.Graphics; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.util.Arrays; /* * JavaFX is not on the default java classpath until Java 8 (but is still included with Java 7), so your IDE will probably complain that the imports below can't be resolved. * In Eclipse hitting'Proceed' when told 'Errors exist in project' should allow you to run Expeditee without any issues (although the JFX Browser widget will not display), * or you can just exclude JfxBrowser, WebParser and JfxbrowserActions from the build path. * * If you are using Ant to build/run, 'ant build' will try to build with JavaFX jar added to the classpath. * If this fails, 'ant build-nojfx' will build with the JfxBrowser, WebParser and JfxbrowserActions excluded from the build path. */ import javafx.animation.AnimationTimer; import javafx.application.Platform; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.concurrent.Worker.State; import javafx.scene.web.WebEngine; import javax.imageio.ImageIO; import javax.swing.JComponent; import netscape.javascript.JSObject; import org.expeditee.gui.Frame; import org.expeditee.gui.FrameCreator; import org.expeditee.gui.FrameIO; import org.expeditee.gui.FrameUtils; import org.expeditee.gui.MessageBay; import org.expeditee.gui.MessageBay.Progress; import org.expeditee.items.ItemUtils; import org.expeditee.items.Justification; import org.expeditee.items.Picture; import org.expeditee.items.Text; import org.w3c.dom.Node; import org.w3c.dom.html.HTMLBodyElement; /** * Methods to convert webpages to Expeditee frames * * @author ngw8 * @author jts21 */ public class WebParser { /** * Loads a webpage and renders it as Expeditee frame(s) * * @param URL * Page to load * @param frame * The Expeditee frame to output the converted page to */ public static void parseURL(final String URL, final Frame frame) { try { Platform.runLater(new Runnable() { @Override public void run() { try { WebEngine webEngine = new WebEngine(URL); loadPage(webEngine, frame); } catch (Exception e) { e.printStackTrace(); } } }); } catch (Exception e) { e.printStackTrace(); } } protected static void loadPage(final WebEngine webEngine, final Frame frame) throws Exception { webEngine.getLoadWorker().stateProperty().addListener(new ChangeListener() { @Override public void changed(ObservableValue ov, State oldState, State newState) { switch (newState) { case READY: // READY // MessageBay.displayMessage("WebEngine ready"); break; case SCHEDULED: // SCHEDULED // MessageBay.displayMessage("Scheduled page load"); break; case RUNNING: // RUNNING System.out.println("Loading page!"); // MessageBay.displayMessage("WebEngine running"); break; case SUCCEEDED: // SUCCEEDED // MessageBay.displayMessage("Finished loading page"); System.out.println("Parsing page!"); webEngine.executeScript("window.resizeTo(800, 800);" + "document.body.style.width = '1000px'"); parsePage(webEngine, frame); System.out.println("Parsed page!"); break; case CANCELLED: // CANCELLED MessageBay.displayMessage("Cancelled loading page"); break; case FAILED: // FAILED MessageBay.displayMessage("Failed to load page"); break; } } }); } /** * Converts a loaded page to Expeditee frame(s) * * @param webEngine * The JavaFX WebEngine in which the page to be converted is loaded * @param frame * The Expeditee frame to output the converted page to */ public static void parsePage(final WebEngine webEngine, final Frame frame) { try { Platform.runLater(new Runnable() { @Override public void run() { try { Progress progressBar = MessageBay.displayProgress("Converting web page"); Node doc = (Node) webEngine.executeScript("document.body"); JSObject window = (JSObject) webEngine.executeScript("window"); frame.setBackgroundColor(rgbStringToColor((String) ((JSObject) (window.call("getComputedStyle", new Object[] { doc }))).call("getPropertyValue", new Object[] { "background-color" }))); // Functions to be used later in JavaScript webEngine.executeScript("" + "function addToSpan(text) {" + " span = document.createElement('wordSpan');" + " span.textContent = text;" + " par.insertBefore(span, refNode);" // Checking if the current word is on a new line (i.e. lower than the previous word) + " if (prevSpan !== null && span.getBoundingClientRect().top > prevSpan.getBoundingClientRect().top) {" // If it is, prepend a new line character to it. The new line characters doesn't affect the rendered HTML + " span.textContent = '\\n' + span.textContent;" // Checking if the previous word is horizontally aligned with the one before it. // If it is, merge the text of the two spans + " if ( prevPrevSpan !== null && prevPrevSpan.getBoundingClientRect().left == prevSpan.getBoundingClientRect().left) {" + " prevPrevSpan.textContent = prevPrevSpan.textContent + prevSpan.textContent;" + " par.removeChild(prevSpan);" + " } else {" + " prevPrevSpan = prevSpan;" + " }" + " prevSpan = span;" + " } else if ( prevSpan !== null) {" // Word is on the same line as the previous one, so merge the second into the span of the first + " prevSpan.textContent = prevSpan.textContent + span.textContent;" + " par.removeChild(span);" + " } else {" + " prevSpan = span;" + " }" + "}" + "function splitIntoWords(toSplit) {" + " var words = [];" + " var pattern = /\\s+/g;" + " var words = toSplit.split(pattern);" + "" + " for (var i = 0; i < words.length - 1; i++) {" + " words[i] = words[i] + ' ';" + " }" + " return words;" + "}" ); // Using Javascript to get an array of all the text nodes in the document so they can be wrapped in spans. Have to // loop through twice (once to build the array and once actually going through the array, otherwise when the // textnode is removed from the document items end up being skipped) JSObject textNodes = (JSObject) webEngine.executeScript("" + "function getTextNodes(rootNode){" + "var node;" + "var textNodes=[];" + "var walk = document.createTreeWalker(rootNode, NodeFilter.SHOW_TEXT);" + "while(node=walk.nextNode()) {" + "if((node.textContent.trim().length > 0)) { " + "textNodes.push(node);" + "}" + "}" + "return textNodes;" + "}; " + "getTextNodes(document.body)" ); int nodesLength = (Integer) textNodes.getMember("length"); // Looping through all the text nodes in the document for (int j = 0; j < nodesLength; j++) { Node currentNode = (Node) textNodes.getSlot(j); // Making the current node accessible in JavaScript window.setMember("currentNode", currentNode); webEngine.executeScript("" + "var span = null, prevSpan = null, prevPrevSpan = null;" // Removing repeated whitespace from the text node's content then splitting it into individual words + "var textContent = currentNode.textContent.replace(/\\n|\\r/g, '').replace(/\\s+/g, ' ');" + "var words = splitIntoWords(textContent);" + "var refNode = currentNode.nextSibling;" + "var par = currentNode.parentElement;" + "currentNode.parentElement.removeChild(currentNode);" + "for (var i = 0; i < words.length; i++) {" + " addToSpan(words[i]);" + "}" + "if (prevPrevSpan !== null && prevPrevSpan.getBoundingClientRect().left == prevSpan.getBoundingClientRect().left) {" + " prevPrevSpan.textContent = prevPrevSpan.textContent + prevSpan.textContent;" + " par.removeChild(prevSpan);" + "}" ); // Will never reach 100% here, as the processing is not quite finished - progress is set to 100% at the end of // the addPageToFrame loop below progressBar.set((100 * (j)) / nodesLength); } // Finding all links within the page, then setting the href attribute of all their descendants to be the same // link/URL. // This is needed because there is no apparent and efficient way to check if an element is a child of a link when // running through the document when added each element to Expeditee webEngine.executeScript("" + "var anchors = document.getElementsByTagName('a');" + "" + "for (var i = 0; i < anchors.length; i++) {" + "var currentAnchor = anchors.item(i);" + "var anchorDescendants = currentAnchor.querySelectorAll('*');" + "for (var j = 0; j < anchorDescendants.length; j++) {" + "anchorDescendants.item(j).href = currentAnchor.href;" + "}" + "}" ); WebParser.addPageToFrame(doc, window, webEngine, frame); progressBar.set(100); } catch (Exception e) { e.printStackTrace(); } System.out.println("Parsed frame"); FrameUtils.Parse(frame); frame.setChanged(true); FrameIO.SaveFrame(frame); } }); } catch (Exception e) { e.printStackTrace(); } } /** * Converts a loaded page to Expeditee frame(s) * * @param webEngine * The JavaFX WebEngine in which the page to be converted is loaded * @param frame * The Expeditee frame to output the converted page to */ public static void parsePageSimple(final WebEngine webEngine, final Object webView, final JComponent jfxPanel, final Frame frame) { try { final Object notifier = new Object(); final MutableBool bottomReached = new MutableBool(false); final Progress progressBar = MessageBay.displayProgress("Converting web page"); AnimationTimer timer = new AnimationTimer() { int frameCount = 0; Frame frameToAddTo = frame; @Override public void handle(long arg0) { // Must wait 2 frames before taking a snapshot of the webview, otherwise JavaFX won't have redrawn if (frameCount++ > 1) { frameCount = 0; this.stop(); BufferedImage image = new BufferedImage(jfxPanel.getWidth(), jfxPanel.getHeight(), BufferedImage.TYPE_INT_ARGB); Graphics graphics = image.createGraphics(); // Drawing the JfxPanel (containing the webview) to the image jfxPanel.paint(graphics); try { int hashcode = Arrays.hashCode(image.getData().getPixels(0, 0, image.getWidth(), image.getHeight(), (int[]) null)); File out = new File(FrameIO.IMAGES_PATH + "webpage-" + Integer.toHexString(hashcode) + ".png"); out.mkdirs(); ImageIO.write(image, "png", out); Text link = new Text("Next"); link.setPosition(500, 20); frameToAddTo.addItem(link); FrameIO.SaveFrame(frameToAddTo); frameToAddTo = FrameIO.CreateFrame(frame.getFramesetName(), Integer.toHexString(hashcode), null); link.setLink(frameToAddTo.getName()); // Adding the image frameToAddTo.addText(0, 0, "@i: " + out.getName(), null); // Button to go to the next page Text nextButton = (Text) FrameCreator.createButton("Next", null, null, 10F, 10F); nextButton.setID(frameToAddTo.getNextItemID()); nextButton.addAction("next"); frameToAddTo.addItem(nextButton); FrameIO.SaveFrame(frameToAddTo); System.out.println("C"); } catch (IOException e) { e.printStackTrace(); } graphics.dispose(); image.flush(); synchronized (notifier) { notifier.notify(); } try { Platform.runLater(new Runnable() { @Override public void run() { try { HTMLBodyElement doc = (HTMLBodyElement) webEngine.executeScript("document.body"); JSObject window = (JSObject) webEngine.executeScript("window"); System.out.println("adding"); WebParser.addPageToFrame(doc, window, webEngine, frameToAddTo); } catch (Exception ex) { ex.printStackTrace(); } } }); } catch (Exception ex) { ex.printStackTrace(); } } } }; Platform.runLater(new Runnable() { @Override public void run() { try { webEngine.executeScript("" // Initializing the counter used when scrolling the page + "var scrollCounter = 0;" // Setting all text to be hidden + "var css = document.createElement('style');" + "css.type = 'text/css';" + "var style = 'WordSpan { visibility: hidden }';" + "css.appendChild(document.createTextNode(style));" + "document.getElementsByTagName('head')[0].appendChild(css);"); HTMLBodyElement doc = (HTMLBodyElement) webEngine.executeScript("document.body"); JSObject window = (JSObject) webEngine.executeScript("window"); frame.setBackgroundColor(rgbStringToColor((String) ((JSObject) (window.call("getComputedStyle", new Object[] { doc }))).call("getPropertyValue", new Object[] { "background-color" }))); // Functions to be used later in JavaScript webEngine.executeScript("" + "function addToSpan(text) {" + " span = document.createElement('wordSpan');" + " span.textContent = text;" + " par.insertBefore(span, refNode);" + " if (prevSpan !== null && span.getBoundingClientRect().top > prevSpan.getBoundingClientRect().top) {" + " span.textContent = '\\n' + span.textContent;" + " if ( prevPrevSpan !== null && prevPrevSpan.getBoundingClientRect().left == prevSpan.getBoundingClientRect().left) {" + " prevPrevSpan.textContent = prevPrevSpan.textContent + prevSpan.textContent;" + " par.removeChild(prevSpan);" + " } else {" + " prevPrevSpan = prevSpan;" + " }" + " prevSpan = span;" + " } else if ( prevSpan !== null) {" + " prevSpan.textContent = prevSpan.textContent + span.textContent;" + " par.removeChild(span);" + " } else {" + " prevSpan = span;" + " }" + "}" + "function splitIntoWords(toSplit) {" + " var words = [];" + " var pattern = /\\s+/g;" + " var words = toSplit.split(pattern);" + "" + " for (var i = 0; i < words.length - 1; i++) {" + " words[i] = words[i] + ' ';" + " }" + " return words;" + "}" ); // Using Javascript to get an array of all the text nodes in the document so they can be wrapped in spans. Have to // loop through twice (once to build the array and once actually going through the array, otherwise when the // textnode is removed from the document items end up being skipped) JSObject textNodes = (JSObject) webEngine.executeScript("" + "function getTextNodes(rootNode){" + "var node;" + "var textNodes=[];" + "var walk = document.createTreeWalker(rootNode, NodeFilter.SHOW_TEXT);" + "while(node=walk.nextNode()) {" + "if((node.textContent.trim().length > 0)) { " + "textNodes.push(node);" + "}" + "}" + "return textNodes;" + "}; " + "getTextNodes(document.body)" ); int nodesLength = (Integer) textNodes.getMember("length"); // Looping through all the text nodes in the document for (int j = 0; j < nodesLength; j++) { Node currentNode = (Node) textNodes.getSlot(j); // Making the current node accessible in JavaScript window.setMember("currentNode", currentNode); webEngine.executeScript("" + "var span = null, prevSpan = null, prevPrevSpan = null;" // Removing repeated whitespace from the text node's content then splitting it into individual words + "var textContent = currentNode.textContent.replace(/\\n|\\r/g, '').replace(/\\s+/g, ' ');" + "var words = splitIntoWords(textContent);" + "var refNode = currentNode.nextSibling;" + "var par = currentNode.parentElement;" + "currentNode.parentElement.removeChild(currentNode);" + "for (var i = 0; i < words.length; i++) {" + " addToSpan(words[i]);" + "}" + "if (prevPrevSpan !== null && prevPrevSpan.getBoundingClientRect().left == prevSpan.getBoundingClientRect().left) {" + " prevPrevSpan.textContent = prevPrevSpan.textContent + prevSpan.textContent;" + " par.removeChild(prevSpan);" + "}" ); // Will never reach 100% here, as the processing is not quite finished - progress is set to 100% at the end of // the addPageToFrame loop below progressBar.set((100 * (j)) / nodesLength); } // Finding all links within the page, then setting the href attribute of all their descendants to be the same // link/URL. // This is needed because there is no apparent and efficient way to check if an element is a child of a link when // running through the document when added each element to Expeditee webEngine.executeScript("" + "var anchors = document.getElementsByTagName('a');" + "" + "for (var i = 0; i < anchors.length; i++) {" + "var currentAnchor = anchors.item(i);" + "var anchorDescendants = currentAnchor.querySelectorAll('*');" + "for (var j = 0; j < anchorDescendants.length; j++) {" + "anchorDescendants.item(j).href = currentAnchor.href;" + "}" + "}" ); } catch (Exception ex) { ex.printStackTrace(); } synchronized (notifier) { notifier.notify(); } } }); synchronized (notifier) { try { // Waiting for the JavaFX thread to finish notifier.wait(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } while (!bottomReached.getValue()) { Platform.runLater(new Runnable() { @Override public void run() { try { // Scrolling down the page webEngine.executeScript("" + "window.scrollTo(0, scrollCounter * window.innerHeight);" + "scrollCounter = scrollCounter+1;"); System.out.println('B'); bottomReached.setValue((Boolean) webEngine.executeScript("(window.pageYOffset + window.innerHeight >= document.documentElement.scrollHeight)")); synchronized (notifier) { notifier.notify(); } } catch (Exception e) { e.printStackTrace(); } } }); synchronized (notifier) { try { // Waiting for the JavaFX thread to finish notifier.wait(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } timer.start(); synchronized (notifier) { try { // Waiting for the timer thread to finish before looping again notifier.wait(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } catch (Exception ex) { ex.printStackTrace(); } } /** * @param rgbString * string in the format rgb(x,x,x) or rgba(x,x,x,x) * @return A Color object that should match the rgb string passed int. Returns null if alpha is 0 */ private static Color rgbStringToColor(String rgbString) { if (rgbString == null) { return null; } // Splitting the string into 'rgb' and 'x, x, x' String[] tmpStrings = rgbString.split("\\(|\\)"); // Splitting up the RGB(A) components into an array tmpStrings = tmpStrings[1].split(","); int[] components = new int[4]; Arrays.fill(components, 255); for (int i = 0; i < tmpStrings.length; i++) { Float d = Float.parseFloat(tmpStrings[i].trim()); components[i] = Math.round(d); } if (components[3] > 0) { return new Color(components[0], components[1], components[2], components[3]); } else { return null; } } /** * @param rootElement * Element that will be converted (including all sub-elements) * @param backgroundColor * String to be used as the background color of this element when added. In the format "rgb(x,x,x)" or "rgba(x,x,x,x)" * @param window * 'window' from Javascript * @param webEngine * Web engine that the page is loaded in * @param frame * Expeditee frame to add the converted page to * @throws IllegalArgumentException * @throws IllegalAccessException */ private static void addPageToFrame(Node rootElement, JSObject window, WebEngine webEngine, Frame frame) throws InvocationTargetException, IllegalAccessException, IllegalArgumentException { Node currentNode = rootElement; if (currentNode.getNodeType() == Node.TEXT_NODE || currentNode.getNodeType() == Node.ELEMENT_NODE) { JSObject style; JSObject bounds; if (currentNode.getNodeType() == Node.TEXT_NODE) { // CSS style for the element style = (JSObject) window.call("getComputedStyle", new Object[] { currentNode.getParentNode() }); // Getting a rectangle that represents the area and position of the element bounds = (JSObject) ((JSObject) currentNode.getParentNode()).call("getBoundingClientRect", new Object[] {}); } else { style = (JSObject) window.call("getComputedStyle", new Object[] { currentNode }); bounds = (JSObject) ((JSObject) currentNode).call("getBoundingClientRect", new Object[] {}); } // Bounding rectangle position is relative to the current view, so scroll position must be added to x/y // TODO: This doesn't check if an element or any of its parent elements have position:fixed set - the only // way to check seems to be to walking through the element's parents until the document root is reached float x = Float.valueOf(bounds.getMember("left").toString()) + Float.valueOf(webEngine.executeScript("window.pageXOffset").toString()); float y = Float.valueOf(bounds.getMember("top").toString()) + Float.valueOf(webEngine.executeScript("window.pageYOffset").toString()); float width = Float.valueOf(bounds.getMember("width").toString()); float height = Float.valueOf(bounds.getMember("height").toString()); // Checking if the element is actually visible on the page if (WebParser.elementVisible(x, y, width, height, style)) { // Filtering the node type, starting with text nodes if (currentNode.getNodeType() == Node.TEXT_NODE) { String fontSize = ((String) style.call("getPropertyValue", new Object[] { "font-size" })); // Trimming off the units (always px) from the font size fontSize = fontSize.substring(0, fontSize.length() - 2); // Always returns in format "rgb(x,x,x)" or "rgba(x,x,x,x)" String color = (String) style.call("getPropertyValue", new Object[] { "color" }); // Always returns in format "rgb(x,x,x)" or "rgba(x,x,x,x)" String bgColorString = (String) style.call("getPropertyValue", new Object[] { "background-color" }); String align = (String) style.call("getPropertyValue", new Object[] { "text-align" }); // Returns comma-separated list of typefaces String typeface = (String) style.call("getPropertyValue", new Object[] { "font-family" }); String[] typefaces = typeface.split(", |,"); String weight = (String) style.call("getPropertyValue", new Object[] { "font-weight" }); String fontStyle = (String) style.call("getPropertyValue", new Object[] { "font-style" }); // Returns "normal" or a value in pixels (e.g. "10px") String letterSpacing = (String) style.call("getPropertyValue", new Object[] { "letter-spacing" }); // Returns a value in pixels (e.g. "10px") String lineHeight = (String) style.call("getPropertyValue", new Object[] { "line-height" }); String textTransform = (String) style.call("getPropertyValue", new Object[] { "text-transform" }); String linkUrl = (String) ((JSObject) currentNode.getParentNode()).getMember("href"); Boolean fontFound = false; Font font = new Font(null); // Looping through all font-families listed in the element's CSS until one that is installed is // found, or the end of the list is reached, in which case the default font is used for (int j = 0; j < typefaces.length && !fontFound; j++) { if (typefaces[j].toLowerCase().equals("sans-serif")) { typefaces[j] = "Arial Unicode MS"; } else if (typefaces[j].toLowerCase().equals("serif")) { typefaces[j] = "Times New Roman"; } else if ((typefaces[j].toLowerCase().equals("arial"))) { // Have to use Arial Unicode, otherwise unicode characters display incorrectly typefaces[j] = "Arial Unicode MS"; } // Regex will remove any inverted commas surrounding multi-word typeface names font = new Font(typefaces[j].replaceAll("^'|'$", ""), Font.PLAIN, 12); // If the font isn't found, Java just uses Font.DIALOG, so this check checks whether the font was found if (!(font.getFamily().toLowerCase().equals(Font.DIALOG.toLowerCase()))) { fontFound = true; } } if (font.getFamily().toLowerCase().equals(Font.DIALOG.toLowerCase())) { font = new Font("Times New Roman", Font.PLAIN, 12); } String fontStyleComplete = ""; int weightInt = 0; try { weightInt = Integer.parseInt(weight); } catch (NumberFormatException nfe) { // Use default value as set above } // checking if font is bold - i.e. 'bold', 'bolder' or weight over 500 if (weight.toLowerCase().startsWith("bold") || weightInt > 500) { fontStyleComplete = fontStyleComplete.concat("bold"); } if (fontStyle.toLowerCase().equals("italic") || fontStyle.toLowerCase().equals("oblique")) { fontStyleComplete = fontStyleComplete.concat("italic"); } float fontSizeFloat = 12; try { fontSizeFloat = Float.valueOf(fontSize); } catch (NumberFormatException nfe) { // Use default value as set above } float letterSpacingFloat = -0.008f; try { letterSpacingFloat = (Integer.parseInt(letterSpacing.substring(0, letterSpacing.length() - 2)) / (fontSizeFloat)); } catch (NumberFormatException nfe) { // Use default value as set above } float lineHeightInt = -1; try { lineHeightInt = (Float.parseFloat(lineHeight.substring(0, lineHeight.length() - 2))); } catch (NumberFormatException nfe) { // Use default value as set above } Text t; String textContent = currentNode.getTextContent().replaceAll("[^\\S\\n]+", " "); textContent = textContent.replaceAll("^(\\s)(\\n|\\r)", ""); if (textTransform.equals("uppercase")) { textContent = textContent.toUpperCase(); } else if (textTransform.equals("lowercase")) { textContent = textContent.toUpperCase(); } // Adding the text to the frame. Expeditee text seems to be positioned relative to the baseline of the first line, so // the font size has to be added to the y-position t = frame.addText(Math.round(x), Math.round(y + fontSizeFloat), textContent, null); t.setColor(rgbStringToColor(color)); t.setBackgroundColor(rgbStringToColor(bgColorString)); t.setFont(font); t.setSize(fontSizeFloat); t.setFontStyle(fontStyleComplete); t.setLetterSpacing(letterSpacingFloat); // Removing any spacing between lines allowing t.getLineHeight() to be used to get the actual height // of just the characters (i.e. distance from ascenders to descenders) t.setSpacing(0); t.setSpacing(lineHeightInt - t.getLineHeight()); if (align.equals("left")) { t.setJustification(Justification.left); } else if (align.equals("right")) { t.setJustification(Justification.right); } else if (align.equals("center")) { t.setJustification(Justification.center); } else if (align.equals("justify")) { t.setJustification(Justification.full); } // Font size is added to the item width to give a little breathing room t.setWidth(Math.round(width + (t.getSize()))); if (!linkUrl.equals("undefined")) { t.setAction("gotourl " + linkUrl); t.setActionMark(false); } } else if (currentNode.getNodeType() == Node.ELEMENT_NODE) { // Always returns in format "rgb(x,x,x)" or "rgba(x,x,x,x)" String bgColorString = (String) style.call("getPropertyValue", new Object[] { "background-color" }); Color bgColor = rgbStringToColor(bgColorString); // If the element has a background color then add it (to Expeditee) as a rectangle with that background color if (bgColor != null) { System.out.println("bg"); frame.addRectangle(Math.round(x), Math.round(y), Math.round(width), Math.round(height), 0, null, bgColor); } // background image, returns in format "url(protocol://absolute/path/to/img.extension)" for images, // may also return gradients, data, etc. (not handled yet). Only need to add bg image on // 'ELEMENT_NODE' (and not 'TEXT_NODE' otherwise there would be double-ups String bgImage = (String) style.call("getPropertyValue", new Object[] { "background-image" }); String linkUrl = (String) ((JSObject) currentNode).getMember("href"); if (bgImage.startsWith("url(")) { bgImage = bgImage.substring(4, bgImage.length() - 1); String bgSize = ((String) style.call("getPropertyValue", new Object[] { "background-size" })).toLowerCase(); String bgRepeat = ((String) style.call("getPropertyValue", new Object[] { "background-repeat" })).toLowerCase(); // Returns "[x]px [y]px", "[x]% [y]%", "[x]px [y]%" or "[x]% [y]px" String bgPosition = ((String) style.call("getPropertyValue", new Object[] { "background-position" })).toLowerCase(); String[] bgOffsetCoords = bgPosition.split(" "); int bgOffsetX = 0, bgOffsetY = 0; float originXPercent = 0, originYPercent = 0; int cropStartX, cropStartY, cropEndX, cropEndY; // Converting the x and y offset values to integers (and from % to px if needed) if (bgOffsetCoords[0].endsWith("%")) { bgOffsetX = (int) ((Integer.valueOf(bgOffsetCoords[0].substring(0, bgOffsetCoords[0].length() - 1)) / 100.0) * width); originXPercent = (Integer.valueOf(bgOffsetCoords[0].substring(0, bgOffsetCoords[0].length() - 1))) / 100f; } else if (bgOffsetCoords[0].endsWith("px")) { bgOffsetX = (int) (Integer.valueOf(bgOffsetCoords[0].substring(0, bgOffsetCoords[0].length() - 2))); } if (bgOffsetCoords[1].endsWith("%")) { bgOffsetY = (int) ((Integer.valueOf(bgOffsetCoords[1].substring(0, bgOffsetCoords[1].length() - 1)) / 100.0) * height); originYPercent = (Integer.valueOf(bgOffsetCoords[1].substring(0, bgOffsetCoords[1].length() - 1))) / 100f; } else if (bgOffsetCoords[1].endsWith("px")) { bgOffsetY = (int) (Integer.valueOf(bgOffsetCoords[1].substring(0, bgOffsetCoords[1].length() - 2))); } // Converting from an offset to crop coords cropStartX = -1 * bgOffsetX; cropEndX = (int) (cropStartX + width); cropStartY = -1 * bgOffsetY; cropEndY = (int) (cropStartY + height); int bgWidth = -1; if (bgSize.equals("cover")) { bgWidth = (int) width; } else if (bgSize.equals("contain")) { // TODO: actually compute the appropriate width bgWidth = (int) width; } else if (bgSize.equals("auto")) { bgWidth = -1; } else { bgSize = bgSize.split(" ")[0]; if (bgSize.endsWith("%")) { bgWidth = (int) ((Integer.parseInt(bgSize.replaceAll("\\D", "")) / 100.0) * width); } else if (bgSize.endsWith("px")) { bgWidth = Integer.parseInt(bgSize.replaceAll("\\D", "")); } } try { WebParser.addImageFromUrl(bgImage, linkUrl, frame, x, y, bgWidth, cropStartX, cropStartY, cropEndX, cropEndY, bgRepeat, originXPercent, originYPercent); } catch (MalformedURLException mue) { // probably a 'data:' url, not supported yet mue.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } String imgSrc; if (currentNode.getNodeName().toLowerCase().equals("img") && (imgSrc = ((JSObject) currentNode).getMember("src").toString()) != null) { try { WebParser.addImageFromUrl(imgSrc, linkUrl, frame, x, y, (int) width, null, null, null, null, null, 0, 0); } catch (MalformedURLException mue) { // probably a 'data:' url, not supported yet mue.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } Node childNode = currentNode.getFirstChild(); while (childNode != null) { addPageToFrame(childNode, window, webEngine, frame); childNode = childNode.getNextSibling(); } } } private static boolean elementVisible(float x, float y, float width, float height, JSObject style) { try { if (width <= 0 || height <= 0 || x + width <= 0 || y + height <= 0 || ((String) style.call("getPropertyValue", new Object[] { "visibility" })).equals("hidden") || ((String) style.call("getPropertyValue", new Object[] { "display" })).equals("none")) { return false; } else { return true; } } catch (Exception e) { e.printStackTrace(); return false; } } /** * @param imgSrc * URL of the image to add * @param linkUrl * Absolute URL that the image should link to when clicked * @param frame * Frame to add the image to * @param x * X-coordinate at which the image should be placed on the frame * @param y * Y-coordinate at which the image should be placed on the frame * @param width * Width of the image once added to the frame. Negative 1 (-1) will cause the actual width of the image file to be used * * @param cropStartX * X-coordinate at which to start crop, or null for no crop * @param cropStartY * Y-coordinate at which to start crop, or null for no crop * @param cropEndX * X-coordinate at which to end the crop, or null for no crop * @param cropEndY * Y-coordinate at which to end the crop, or null for no crop * * @param repeat * String determining how the image should be tiled/repeated. Valid strings are: no-repeat, repeat-x, or * repeat-y. All other values (including null) will cause the image to repeat in both directions * * @param originXPercent * Percentage into the image to use as the x coordinate of the image's origin point * @param originYPercent * Percentage into the image to use as the y coordinate of the image's origin point * * @throws MalformedURLException * @throws IOException */ private static void addImageFromUrl(String imgSrc, String linkUrl, final Frame frame, float x, float y, int width, Integer cropStartX, Integer cropStartY, Integer cropEndX, Integer cropEndY, String repeat, float originXPercent, float originYPercent) throws MalformedURLException, IOException { URL imgUrl = new URL(imgSrc); HttpURLConnection connection = (HttpURLConnection) (imgUrl.openConnection()); // Spoofing a widely accepted User Agent, since some sites refuse to serve non-webbrowser clients connection.setRequestProperty("User-Agent", "Mozilla/5.0"); BufferedImage img = ImageIO.read(connection.getInputStream()); int hashcode = Arrays.hashCode(img.getData().getPixels(0, 0, img.getWidth(), img.getHeight(), (int[]) null)); File out = new File(FrameIO.IMAGES_PATH + Integer.toHexString(hashcode) + ".png"); out.mkdirs(); ImageIO.write(img, "png", out); if (cropEndX == null || cropStartX == null || cropEndY == null || cropStartY == null) { cropStartX = 0; cropStartY = 0; cropEndX = img.getWidth(); cropEndY = img.getHeight(); } else if (cropStartX < 0) { cropEndX = cropEndX - cropStartX; x = x + Math.abs(cropStartX); cropStartX = 0; } if (cropStartY < 0) { cropEndY = cropEndY - cropStartY; y = y + Math.abs(cropStartY); cropStartY = 0; } if (width < 0) { width = img.getWidth(); } if (repeat != null) { if (repeat.equals("no-repeat")) { int tmpCropEndY = (int) (cropStartY + ((float) width / img.getWidth()) * img.getHeight()); int tmpCropEndX = cropStartX + width; cropEndX = (cropEndX < tmpCropEndX) ? cropEndX : tmpCropEndX; cropEndY = (cropEndY < tmpCropEndY) ? cropEndY : tmpCropEndY; } else if (repeat.equals("repeat-x")) { int tmpCropEndY = (int) (cropStartY + ((float) width / img.getWidth()) * img.getHeight()); cropEndY = (cropEndY < tmpCropEndY) ? cropEndY : tmpCropEndY; } else if (repeat.equals("repeat-y")) { int tmpCropEndX = cropStartX + width; cropEndX = (cropEndX < tmpCropEndX) ? cropEndX : tmpCropEndX; } } if (originXPercent > 0) { int actualWidth = cropEndX - cropStartX; int originXPixels = Math.round(originXPercent * actualWidth); x = x - originXPixels; cropStartX = (int) (cropStartX + (width - actualWidth) * originXPercent); cropEndX = (int) (cropEndX + (width - actualWidth) * originXPercent); } if (originYPercent > 0) { int height = (int) ((img.getHeight() / (float) img.getWidth()) * width); int actualHeight = (cropEndY - cropStartY); int originYPixels = Math.round(originYPercent * actualHeight); y = y - originYPixels; cropStartY = (int) (cropStartY + (height - actualHeight) * originYPercent); cropEndY = (int) (cropEndY + (height - actualHeight) * originYPercent); } Text text = new Text("@i: " + out.getName() + " " + width); text.setPosition(x, y); Picture pic = ItemUtils.CreatePicture(text, frame); float invScale = 1 / pic.getScale(); pic.setCrop((int)(cropStartX * invScale), (int)(cropStartY * invScale), (int)(cropEndX * invScale), (int)(cropEndY * invScale)); if (linkUrl != null && !linkUrl.equals("undefined")) { pic.setAction("goto " + linkUrl); pic.setActionMark(false); } frame.addItem(pic); pic.anchor(); pic.getSource().anchor(); } private static class MutableBool { private boolean value; public MutableBool(boolean value) { this.value = value; } public boolean getValue() { return value; } public void setValue(boolean value) { this.value = value; } } }