1 | package org.expeditee.io;
|
---|
2 |
|
---|
3 | import java.awt.Color;
|
---|
4 | import java.awt.Font;
|
---|
5 | import java.awt.image.BufferedImage;
|
---|
6 | import java.io.File;
|
---|
7 | import java.io.IOException;
|
---|
8 | import java.net.HttpURLConnection;
|
---|
9 | import java.net.MalformedURLException;
|
---|
10 | import java.net.URL;
|
---|
11 | import java.util.ArrayList;
|
---|
12 | import java.util.Arrays;
|
---|
13 | import java.util.regex.Matcher;
|
---|
14 | import java.util.regex.Pattern;
|
---|
15 |
|
---|
16 | import javax.imageio.ImageIO;
|
---|
17 |
|
---|
18 | import org.expeditee.gui.Frame;
|
---|
19 | import org.expeditee.gui.FrameIO;
|
---|
20 | import org.expeditee.gui.FrameUtils;
|
---|
21 | import org.expeditee.gui.MessageBay;
|
---|
22 | import org.expeditee.gui.MessageBay.Progress;
|
---|
23 | import org.expeditee.items.ItemUtils;
|
---|
24 | import org.expeditee.items.Justification;
|
---|
25 | import org.expeditee.items.Picture;
|
---|
26 | import org.expeditee.items.Text;
|
---|
27 | import org.expeditee.reflection.JavaFX;
|
---|
28 | import org.w3c.dom.Element;
|
---|
29 | import org.w3c.dom.Node;
|
---|
30 | import org.w3c.dom.html.HTMLBodyElement;
|
---|
31 |
|
---|
32 | /**
|
---|
33 | * Methods to convert webpages to Expeditee frames
|
---|
34 | *
|
---|
35 | * @author ngw8
|
---|
36 | * @author jts21
|
---|
37 | */
|
---|
38 | public class WebParser {
|
---|
39 |
|
---|
40 |
|
---|
41 | /**
|
---|
42 | * Loads a webpage and renders it as Expeditee frame(s)
|
---|
43 | *
|
---|
44 | * @param URL
|
---|
45 | * Page to load
|
---|
46 | * @param frame
|
---|
47 | * The Expeditee frame to output the converted page to
|
---|
48 | */
|
---|
49 | public static void parseURL(final String URL, final Frame frame) {
|
---|
50 | try {
|
---|
51 | JavaFX.PlatformRunLater.invoke(null, new Runnable() {
|
---|
52 | @Override
|
---|
53 | public void run() {
|
---|
54 | try {
|
---|
55 | Object webEngine = JavaFX.WebEngineConstructor.newInstance(URL);
|
---|
56 | loadPage(webEngine, frame);
|
---|
57 | } catch (Exception e) {
|
---|
58 | e.printStackTrace();
|
---|
59 | }
|
---|
60 | }
|
---|
61 | });
|
---|
62 | } catch (Exception e) {
|
---|
63 | e.printStackTrace();
|
---|
64 | }
|
---|
65 | }
|
---|
66 |
|
---|
67 | protected static void loadPage(final Object webEngine, final Frame frame) throws Exception {
|
---|
68 | JavaFX.ReadOnlyObjectPropertyAddListener.invoke(JavaFX.WorkerStateProperty.invoke(JavaFX.WebEngineGetLoadWorker
|
---|
69 | .invoke(webEngine)), java.lang.reflect.Proxy.newProxyInstance(
|
---|
70 | JavaFX.ChangeListener.getClassLoader(), new java.lang.Class[] { JavaFX.ChangeListener },
|
---|
71 | new java.lang.reflect.InvocationHandler() {
|
---|
72 | @Override
|
---|
73 | public Object invoke(Object proxy, java.lang.reflect.Method method, Object[] args)
|
---|
74 | throws java.lang.Throwable {
|
---|
75 | String method_name = method.getName();
|
---|
76 | // Class<?>[] classes = method.getParameterTypes();
|
---|
77 | // public void changed(ObservableValue ov, State oldState, State newState)
|
---|
78 | if (method_name.equals("changed")) {
|
---|
79 | // changed takes 3 args
|
---|
80 | if (args == null || args.length != 3) {
|
---|
81 | return null;
|
---|
82 | }
|
---|
83 | // args[0] is the ObservableValue
|
---|
84 | // args[2] is the new State
|
---|
85 | if (args[2].getClass() == JavaFX.State) {
|
---|
86 | int id = JavaFX.StateConstants.indexOf(args[2]);
|
---|
87 | switch (id) {
|
---|
88 | case 0: // READY
|
---|
89 | // MessageBay.displayMessage("WebEngine ready");
|
---|
90 | break;
|
---|
91 | case 1: // SCHEDULED
|
---|
92 | // MessageBay.displayMessage("Scheduled page load");
|
---|
93 | break;
|
---|
94 | case 2: // RUNNING
|
---|
95 | System.out.println("Loading page!");
|
---|
96 | // MessageBay.displayMessage("WebEngine running");
|
---|
97 | break;
|
---|
98 | case 3: // SUCCEEDED
|
---|
99 | // MessageBay.displayMessage("Finished loading page");
|
---|
100 | System.out.println("Parsing page!");
|
---|
101 | JavaFX.WebEngineExecuteScript.invoke(webEngine, "window.resizeTo(800, 800)");
|
---|
102 | parsePage(webEngine, frame);
|
---|
103 | System.out.println("Parsed page!");
|
---|
104 | break;
|
---|
105 | case 4: // CANCELLED
|
---|
106 | MessageBay.displayMessage("Cancelled loading page");
|
---|
107 | break;
|
---|
108 | case 5: // FAILED
|
---|
109 | MessageBay.displayMessage("Failed to load page");
|
---|
110 | break;
|
---|
111 | }
|
---|
112 | }
|
---|
113 | System.out.println("\n");
|
---|
114 | }
|
---|
115 | return null;
|
---|
116 | }
|
---|
117 | }));
|
---|
118 | }
|
---|
119 |
|
---|
120 | /**
|
---|
121 | * Converts a loaded page to Expeditee frame(s)
|
---|
122 | *
|
---|
123 | * @param webEngine
|
---|
124 | * The JavaFX WebEngine in which the page to be converted is loaded
|
---|
125 | * @param frame
|
---|
126 | * The Expeditee frame to output the converted page to
|
---|
127 | */
|
---|
128 | public static void parsePage(final Object webEngine, final Frame frame) {
|
---|
129 | try {
|
---|
130 | JavaFX.PlatformRunLater.invoke(null, new Runnable() {
|
---|
131 | @Override
|
---|
132 | public void run() {
|
---|
133 | try {
|
---|
134 | Progress progressBar = MessageBay.displayProgress("Converting web page");
|
---|
135 |
|
---|
136 | HTMLBodyElement doc = (HTMLBodyElement) JavaFX.WebEngineExecuteScript.invoke(webEngine, "document.body");
|
---|
137 |
|
---|
138 | Object window = JavaFX.WebEngineExecuteScript.invoke(webEngine, "window");
|
---|
139 |
|
---|
140 | JavaFX.WebEngineExecuteScript.invoke(webEngine, ""
|
---|
141 | + "var css = 'a * { outline: 0.1px outset rgba(9,9,9,0.001); }';" // TODO
|
---|
142 | + "var head = document.head;"
|
---|
143 | + "var style = document.createElement('style');"
|
---|
144 | + "style.id = 'expediteeparser';"
|
---|
145 |
|
---|
146 | + "style.appendChild(document.createTextNode(css));"
|
---|
147 |
|
---|
148 | + "head.appendChild(style);"
|
---|
149 | );
|
---|
150 |
|
---|
151 | frame.setBackgroundColor(rgbStringToColor((String) JavaFX.JSObjectCall.invoke(JavaFX.JSObjectCall.invoke(window, "getComputedStyle", new Object[] { doc }), "getPropertyValue",
|
---|
152 | new Object[] { "background-color" })));
|
---|
153 |
|
---|
154 | // Functions to be used later in JavaScript
|
---|
155 | JavaFX.WebEngineExecuteScript.invoke(webEngine, ""
|
---|
156 | + "function addToSpan(text) {"
|
---|
157 | + " span = document.createElement('wordSpan');"
|
---|
158 | + " span.textContent = text;"
|
---|
159 | + " par.insertBefore(span, refNode);"
|
---|
160 | + " if (prevSpan !== null && span.getBoundingClientRect().top > prevSpan.getBoundingClientRect().top) {"
|
---|
161 | + " span.textContent = '\\n' + span.textContent;"
|
---|
162 | + " if ( prevPrevSpan !== null && prevPrevSpan.getBoundingClientRect().left == prevSpan.getBoundingClientRect().left) {"
|
---|
163 | + " prevPrevSpan.textContent = prevPrevSpan.textContent + prevSpan.textContent;"
|
---|
164 | + " par.removeChild(prevSpan);"
|
---|
165 | + " } else {"
|
---|
166 | + " prevPrevSpan = prevSpan;"
|
---|
167 | + " }"
|
---|
168 | + " prevSpan = span;"
|
---|
169 | + " } else if ( prevSpan !== null) {"
|
---|
170 | + " prevSpan.textContent = prevSpan.textContent + span.textContent;"
|
---|
171 | + " par.removeChild(span);"
|
---|
172 | + " } else {"
|
---|
173 | + " prevSpan = span;"
|
---|
174 | + " }"
|
---|
175 | + "}"
|
---|
176 | );
|
---|
177 |
|
---|
178 | // Getting an array of all HTML elements in the page
|
---|
179 | Object contentElements = JavaFX.WebEngineExecuteScript.invoke(webEngine, "document.querySelectorAll('body *');");
|
---|
180 | int contentElementsLength = (Integer) JavaFX.JSObjectGetMember.invoke(contentElements, "length");
|
---|
181 |
|
---|
182 | for (int i = 0; i < contentElementsLength; i++) {
|
---|
183 | // Getting the current HTML element, then making it accessible in JavaScript
|
---|
184 | Element currentElement = (Element) JavaFX.JSObjectGetSlot.invoke(contentElements, i);
|
---|
185 | JavaFX.JSObjectSetMember.invoke(window, "para", currentElement);
|
---|
186 |
|
---|
187 | JavaFX.WebEngineExecuteScript.invoke(webEngine, "para.style.wordBreak = 'normal';");
|
---|
188 |
|
---|
189 | // Creating a TreeWalker that is used to loop over all the TextNodes within the current element
|
---|
190 | JavaFX.WebEngineExecuteScript.invoke(webEngine, "var walker = document.createTreeWalker(para, NodeFilter.SHOW_TEXT, null, false);");
|
---|
191 |
|
---|
192 | // Using Javascript to get an array of all the text nodes in the current element. Have to loop through twice (once
|
---|
193 | // to build the array and once actually going through the array, otherwise when the textnode is removed from the
|
---|
194 | // document items end up being skipped)
|
---|
195 | Object textNodes = JavaFX.WebEngineExecuteScript.invoke(webEngine, ""
|
---|
196 | + "function getTextNodes(rootNode){"
|
---|
197 | + "var node;"
|
---|
198 | + "var textNodes=[];"
|
---|
199 | + "var walk = document.createTreeWalker(rootNode, NodeFilter.SHOW_TEXT);"
|
---|
200 | + "while(node=walk.nextNode()) {"
|
---|
201 | + "if((node.textContent.trim().length > 0)) { "
|
---|
202 | + "textNodes.push(node);"
|
---|
203 | + "}"
|
---|
204 | + "}"
|
---|
205 | + "return textNodes;"
|
---|
206 | + "}; "
|
---|
207 | + "getTextNodes(para)");
|
---|
208 |
|
---|
209 | int nodesLength = (Integer) JavaFX.JSObjectGetMember.invoke(textNodes, "length");
|
---|
210 |
|
---|
211 | // Looping through all the text nodes in the current paragraph
|
---|
212 | for (int j = 0; j < nodesLength; j++) {
|
---|
213 | Node currentNode = (Node) JavaFX.JSObjectGetSlot.invoke(textNodes, j);
|
---|
214 |
|
---|
215 | // Making the current node accessible in JavaScript
|
---|
216 | JavaFX.JSObjectSetMember.invoke(window, "textNode", currentNode);
|
---|
217 |
|
---|
218 | JavaFX.WebEngineExecuteScript.invoke(webEngine, ""
|
---|
219 | + "var span = null;"
|
---|
220 | + "var prevSpan = null;"
|
---|
221 | + "var prevPrevSpan = null;"
|
---|
222 | );
|
---|
223 |
|
---|
224 | // Splitting the text node's content into individual words
|
---|
225 | String textContent = ((String) JavaFX.WebEngineExecuteScript.invoke(webEngine, "textNode.textContent")).replaceAll("\\n|\\r", "").replaceAll("\\s+", " ");
|
---|
226 | String[] words = splitIntoWords(textContent);
|
---|
227 |
|
---|
228 | JavaFX.WebEngineExecuteScript.invoke(webEngine, ""
|
---|
229 | + "var refNode = textNode.nextSibling;"
|
---|
230 | + "var par = textNode.parentElement;"
|
---|
231 | + "textNode.parentElement.removeChild(textNode)");
|
---|
232 |
|
---|
233 | // Adding each word back to the page
|
---|
234 | for (int k = 0; k < words.length; k++) {
|
---|
235 | Object currentWord = words[k];
|
---|
236 | JavaFX.JSObjectCall.invoke(window, "addToSpan", new Object[] { currentWord });
|
---|
237 | }
|
---|
238 |
|
---|
239 | JavaFX.WebEngineExecuteScript.invoke(webEngine, ""
|
---|
240 | + " if (prevPrevSpan !== null && prevPrevSpan.getBoundingClientRect().left == prevSpan.getBoundingClientRect().left) {"
|
---|
241 | + " prevPrevSpan.textContent = prevPrevSpan.textContent + prevSpan.textContent;"
|
---|
242 | + " par.removeChild(prevSpan);"
|
---|
243 | + " }"
|
---|
244 | );
|
---|
245 | }
|
---|
246 |
|
---|
247 | progressBar.set((100 * (i + 1)) / contentElementsLength);
|
---|
248 | }
|
---|
249 |
|
---|
250 | // Finding all links within the page, then setting the href attribute of all their descendants to be the same
|
---|
251 | // link/URL.
|
---|
252 | // This is needed because there is no apparent and efficient way to check if an element is a child of a link when
|
---|
253 | // running through the document when added each element to Expeditee
|
---|
254 | JavaFX.WebEngineExecuteScript.invoke(webEngine, ""
|
---|
255 | + "var anchors = document.getElementsByTagName('a');"
|
---|
256 | + ""
|
---|
257 | + "for (var i = 0; i < anchors.length; i++) {"
|
---|
258 | + "var currentAnchor = anchors.item(i);"
|
---|
259 | + "var anchorDescendants = currentAnchor.querySelectorAll('*');"
|
---|
260 | + "for (var j = 0; j < anchorDescendants.length; j++) {"
|
---|
261 | + "anchorDescendants.item(j).href = currentAnchor.href;"
|
---|
262 | + "}"
|
---|
263 | + "}"
|
---|
264 | );
|
---|
265 |
|
---|
266 | // Creating a TreeWalker that is used to loop over all the nodes within the document
|
---|
267 | JavaFX.WebEngineExecuteScript.invoke(webEngine, "var walker = document.createTreeWalker(document.body, NodeFilter.SHOW_ALL);");
|
---|
268 |
|
---|
269 | Node currentNode;
|
---|
270 |
|
---|
271 | // Looping through all the nodes in the document
|
---|
272 | while ((currentNode = (Node) JavaFX.WebEngineExecuteScript.invoke(webEngine, "walker.nextNode()")) != null) {
|
---|
273 |
|
---|
274 | if (currentNode.getNodeType() == Node.TEXT_NODE || currentNode.getNodeType() == Node.ELEMENT_NODE) {
|
---|
275 |
|
---|
276 | Object style;
|
---|
277 | Object bounds;
|
---|
278 |
|
---|
279 | if (currentNode.getNodeType() == Node.TEXT_NODE) {
|
---|
280 | // CSS style for the element
|
---|
281 | style = JavaFX.JSObjectCall.invoke(window, "getComputedStyle", new Object[] { currentNode.getParentNode() });
|
---|
282 |
|
---|
283 | // Getting a rectangle that represents the area and position of the element
|
---|
284 | bounds = JavaFX.JSObjectCall.invoke(currentNode.getParentNode(), "getBoundingClientRect", new Object[] {});
|
---|
285 | } else {
|
---|
286 | style = JavaFX.JSObjectCall.invoke(window, "getComputedStyle", new Object[] { currentNode });
|
---|
287 |
|
---|
288 | bounds = JavaFX.JSObjectCall.invoke(currentNode, "getBoundingClientRect", new Object[] {});
|
---|
289 | }
|
---|
290 |
|
---|
291 | // Bounding rectangle position is relative to the current view, so scroll position must be added to x/y
|
---|
292 | // TODO: This doesn't check if an element or any of its parent elements have position:fixed set - the only
|
---|
293 | // way to check seems to be to walking through the element's parents until the document root is reached
|
---|
294 | float x = Float.valueOf(JavaFX.JSObjectGetMember.invoke(bounds, "left").toString())
|
---|
295 | + Float.valueOf(JavaFX.WebEngineExecuteScript.invoke(webEngine, "window.pageXOffset").toString());
|
---|
296 | float y = Float.valueOf(JavaFX.JSObjectGetMember.invoke(bounds, "top").toString())
|
---|
297 | + Float.valueOf(JavaFX.WebEngineExecuteScript.invoke(webEngine, "window.pageYOffset").toString());
|
---|
298 |
|
---|
299 | float width = Float.valueOf(JavaFX.JSObjectGetMember.invoke(bounds, "width").toString());
|
---|
300 | float height = Float.valueOf(JavaFX.JSObjectGetMember.invoke(bounds, "height").toString());
|
---|
301 |
|
---|
302 | // Checking if the element is actually visible on the page
|
---|
303 | if (WebParser.elementVisible(x, y, width, height, style)) {
|
---|
304 |
|
---|
305 | // Filtering the node type, starting with text nodes
|
---|
306 | if (currentNode.getNodeType() == Node.TEXT_NODE) {
|
---|
307 | String fontSize = ((String) JavaFX.JSObjectCall.invoke(style, "getPropertyValue", new Object[] { "font-size" }));
|
---|
308 |
|
---|
309 | // Trimming off the units (always px) from the font size
|
---|
310 | fontSize = fontSize.substring(0, fontSize.length() - 2);
|
---|
311 |
|
---|
312 | // Always returns in format "rgb(x,x,x)" or "rgba(x,x,x,x)"
|
---|
313 | String color = (String) JavaFX.JSObjectCall.invoke(style, "getPropertyValue", new Object[] { "color" });
|
---|
314 |
|
---|
315 | // Always returns in format "rgb(x,x,x)" or "rgba(x,x,x,x)"
|
---|
316 | String bgColor = (String) JavaFX.JSObjectCall.invoke(style, "getPropertyValue", new Object[] { "background-color" });
|
---|
317 |
|
---|
318 | String align = (String) JavaFX.JSObjectCall.invoke(style, "getPropertyValue", new Object[] { "text-align" });
|
---|
319 |
|
---|
320 | // Returns comma-separated list of typefaces
|
---|
321 | String typeface = (String) JavaFX.JSObjectCall.invoke(style, "getPropertyValue", new Object[] { "font-family" });
|
---|
322 |
|
---|
323 | String[] typefaces = typeface.split(", |,");
|
---|
324 |
|
---|
325 | String weight = (String) JavaFX.JSObjectCall.invoke(style, "getPropertyValue", new Object[] { "font-weight" });
|
---|
326 |
|
---|
327 | String fontStyle = (String) JavaFX.JSObjectCall.invoke(style, "getPropertyValue", new Object[] { "font-style" });
|
---|
328 |
|
---|
329 | // Returns "normal" or a value in pixels (e.g. "10px")
|
---|
330 | String letterSpacing = (String) JavaFX.JSObjectCall.invoke(style, "getPropertyValue", new Object[] { "letter-spacing" });
|
---|
331 |
|
---|
332 | // Returns a value in pixels (e.g. "10px")
|
---|
333 | String lineHeight = (String) JavaFX.JSObjectCall.invoke(style, "getPropertyValue", new Object[] { "line-height" });
|
---|
334 |
|
---|
335 | String textTransform = (String) JavaFX.JSObjectCall.invoke(style, "getPropertyValue", new Object[] { "text-transform" });
|
---|
336 |
|
---|
337 | String linkUrl = (String) JavaFX.JSObjectGetMember.invoke(currentNode.getParentNode(), "href");
|
---|
338 |
|
---|
339 | Boolean fontFound = false;
|
---|
340 | Font font = new Font(null);
|
---|
341 |
|
---|
342 | // Looping through all font-families listed in the element's CSS until one that is installed is
|
---|
343 | // found, or the end of the list is reached, in which case the default font is used
|
---|
344 | for (int j = 0; j < typefaces.length && !fontFound; j++) {
|
---|
345 | if (typefaces[j].toLowerCase().equals("sans-serif")) {
|
---|
346 | typefaces[j] = "Arial";
|
---|
347 | } else if (typefaces[j].toLowerCase().equals("serif")) {
|
---|
348 | typefaces[j] = "Times New Roman";
|
---|
349 | }
|
---|
350 |
|
---|
351 | // Regex will remove any inverted commas surrounding multi-word typeface names
|
---|
352 | font = new Font(typefaces[j].replaceAll("^'|'$", ""), Font.PLAIN, 12);
|
---|
353 |
|
---|
354 | // If the font isn't found, Java just uses Font.DIALOG, so this check checks whether the font was found
|
---|
355 | if (!(font.getFamily().toLowerCase().equals(Font.DIALOG.toLowerCase()))) {
|
---|
356 | fontFound = true;
|
---|
357 | }
|
---|
358 | }
|
---|
359 |
|
---|
360 | if (font.getFamily().toLowerCase().equals(Font.DIALOG.toLowerCase())) {
|
---|
361 | font = new Font("Times New Roman", Font.PLAIN, 12);
|
---|
362 | }
|
---|
363 |
|
---|
364 | String fontStyleComplete = "";
|
---|
365 |
|
---|
366 | int weightInt = 0;
|
---|
367 |
|
---|
368 | try {
|
---|
369 | weightInt = Integer.parseInt(weight);
|
---|
370 | } catch (NumberFormatException nfe) {
|
---|
371 | // Use default value as set above
|
---|
372 | }
|
---|
373 |
|
---|
374 | // checking if font is bold - i.e. 'bold', 'bolder' or weight over 500
|
---|
375 | if (weight.toLowerCase().startsWith("bold") || weightInt > 500) {
|
---|
376 | fontStyleComplete = fontStyleComplete.concat("bold");
|
---|
377 | }
|
---|
378 |
|
---|
379 | if (fontStyle.toLowerCase().equals("italic") || fontStyle.toLowerCase().equals("oblique")) {
|
---|
380 | fontStyleComplete = fontStyleComplete.concat("italic");
|
---|
381 | }
|
---|
382 |
|
---|
383 | float fontSizeFloat = 12;
|
---|
384 |
|
---|
385 | try {
|
---|
386 | fontSizeFloat = Float.valueOf(fontSize);
|
---|
387 | } catch (NumberFormatException nfe) {
|
---|
388 | // Use default value as set above
|
---|
389 | }
|
---|
390 |
|
---|
391 | float letterSpacingFloat = -0.008f;
|
---|
392 |
|
---|
393 | try {
|
---|
394 | letterSpacingFloat = (Integer.parseInt(letterSpacing.substring(0, letterSpacing.length() - 2)) / (fontSizeFloat));
|
---|
395 | } catch (NumberFormatException nfe) {
|
---|
396 | // Use default value as set above
|
---|
397 | }
|
---|
398 |
|
---|
399 | float lineHeightInt = -1;
|
---|
400 |
|
---|
401 | try {
|
---|
402 | lineHeightInt = (Float.parseFloat(lineHeight.substring(0, lineHeight.length() - 2)));
|
---|
403 | } catch (NumberFormatException nfe) {
|
---|
404 | // Use default value as set above
|
---|
405 | }
|
---|
406 |
|
---|
407 | Text t;
|
---|
408 |
|
---|
409 | String textContent = currentNode.getTextContent().replaceAll("[^\\S\\n]+", " ");
|
---|
410 | textContent = textContent.replaceAll("^(\\s)(\\n|\\r)", "");
|
---|
411 |
|
---|
412 | if (textTransform.equals("uppercase")) {
|
---|
413 | textContent = textContent.toUpperCase();
|
---|
414 | } else if (textTransform.equals("lowercase")) {
|
---|
415 | textContent = textContent.toUpperCase();
|
---|
416 | }
|
---|
417 |
|
---|
418 | t = frame.addText(Math.round(x), Math.round(y), textContent, null);
|
---|
419 |
|
---|
420 | t.setColor(rgbStringToColor(color));
|
---|
421 | t.setBackgroundColor(rgbStringToColor(bgColor));
|
---|
422 | t.setFont(font);
|
---|
423 | t.setSize(fontSizeFloat);
|
---|
424 | t.setFontStyle(fontStyleComplete);
|
---|
425 | t.setLetterSpacing(letterSpacingFloat);
|
---|
426 |
|
---|
427 | // Removing any spacing between lines allowing t.getLineHeight() to be used to get the actual height
|
---|
428 | // of just the characters (i.e. distance from ascenders to descenders)
|
---|
429 | t.setSpacing(0);
|
---|
430 |
|
---|
431 | t.setSpacing(lineHeightInt - t.getLineHeight());
|
---|
432 |
|
---|
433 | if (align.equals("left")) {
|
---|
434 | t.setJustification(Justification.left);
|
---|
435 | } else if (align.equals("right")) {
|
---|
436 | t.setJustification(Justification.right);
|
---|
437 | } else if (align.equals("center")) {
|
---|
438 | t.setJustification(Justification.center);
|
---|
439 | } else if (align.equals("justify")) {
|
---|
440 | t.setJustification(Justification.full);
|
---|
441 | }
|
---|
442 |
|
---|
443 | // Font size is added to the item width to give a little breathing room
|
---|
444 | t.setWidth(Math.round(width + (t.getSize())));
|
---|
445 |
|
---|
446 | if (!linkUrl.equals("undefined")) {
|
---|
447 | t.setAction("gotourl " + linkUrl);
|
---|
448 | t.setActionMark(false);
|
---|
449 | }
|
---|
450 |
|
---|
451 | } else if (currentNode.getNodeType() == Node.ELEMENT_NODE) {
|
---|
452 |
|
---|
453 | // background image, returns in format "url(protocol://absolute/path/to/img.extension)" for images,
|
---|
454 | // may also return gradients, data, etc. (not handled yet). Only need to add bg image on
|
---|
455 | // 'ELEMENT_NODE' (and not 'TEXT_NODE' otherwise there would be double-ups
|
---|
456 | String bgImage = (String) JavaFX.JSObjectCall.invoke(style, "getPropertyValue", new Object[] { "background-image" });
|
---|
457 |
|
---|
458 | String linkUrl = (String) JavaFX.JSObjectGetMember.invoke(currentNode, "href");
|
---|
459 |
|
---|
460 | if (bgImage.startsWith("url(")) {
|
---|
461 |
|
---|
462 | bgImage = bgImage.substring(4, bgImage.length() - 1);
|
---|
463 |
|
---|
464 | String bgSize = ((String) JavaFX.JSObjectCall.invoke(style, "getPropertyValue", new Object[] { "background-size" })).toLowerCase();
|
---|
465 | String bgRepeat = ((String) JavaFX.JSObjectCall.invoke(style, "getPropertyValue", new Object[] { "background-repeat" })).toLowerCase();
|
---|
466 |
|
---|
467 | // Returns "[x]px [y]px", "[x]% [y]%", "[x]px [y]%" or "[x]% [y]px"
|
---|
468 | String bgPosition = ((String) JavaFX.JSObjectCall.invoke(style, "getPropertyValue", new Object[] { "background-position" })).toLowerCase();
|
---|
469 |
|
---|
470 | String[] bgOffsetCoords = bgPosition.split(" ");
|
---|
471 |
|
---|
472 | int bgOffsetX = 0, bgOffsetY = 0;
|
---|
473 |
|
---|
474 | float originXPercent = 0, originYPercent = 0;
|
---|
475 |
|
---|
476 | int cropStartX, cropStartY, cropEndX, cropEndY;
|
---|
477 |
|
---|
478 | // Converting the x and y offset values to integers (and from % to px if needed)
|
---|
479 | if (bgOffsetCoords[0].endsWith("%")) {
|
---|
480 | bgOffsetX = (int) ((Integer.valueOf(bgOffsetCoords[0].substring(0, bgOffsetCoords[0].length() - 1)) / 100.0) * width);
|
---|
481 | originXPercent = (Integer.valueOf(bgOffsetCoords[0].substring(0, bgOffsetCoords[0].length() - 1))) / 100f;
|
---|
482 | } else if (bgOffsetCoords[0].endsWith("px")) {
|
---|
483 | bgOffsetX = (int) (Integer.valueOf(bgOffsetCoords[0].substring(0, bgOffsetCoords[0].length() - 2)));
|
---|
484 | }
|
---|
485 |
|
---|
486 | if (bgOffsetCoords[1].endsWith("%")) {
|
---|
487 | bgOffsetY = (int) ((Integer.valueOf(bgOffsetCoords[1].substring(0, bgOffsetCoords[1].length() - 1)) / 100.0) * height);
|
---|
488 | originYPercent = (Integer.valueOf(bgOffsetCoords[1].substring(0, bgOffsetCoords[1].length() - 1))) / 100f;
|
---|
489 | } else if (bgOffsetCoords[1].endsWith("px")) {
|
---|
490 | bgOffsetY = (int) (Integer.valueOf(bgOffsetCoords[1].substring(0, bgOffsetCoords[1].length() - 2)));
|
---|
491 | }
|
---|
492 |
|
---|
493 | // Converting from an offset to crop coords
|
---|
494 | cropStartX = -1 * bgOffsetX;
|
---|
495 | cropEndX = (int) (cropStartX + width);
|
---|
496 |
|
---|
497 | cropStartY = -1 * bgOffsetY;
|
---|
498 | cropEndY = (int) (cropStartY + height);
|
---|
499 |
|
---|
500 | int bgWidth = -1;
|
---|
501 |
|
---|
502 | if (bgSize.equals("cover")) {
|
---|
503 | bgWidth = (int) width;
|
---|
504 | } else if (bgSize.equals("contain")) {
|
---|
505 | // TODO: actually compute the appropriate width
|
---|
506 | bgWidth = (int) width;
|
---|
507 | } else if (bgSize.equals("auto")) {
|
---|
508 | bgWidth = -1;
|
---|
509 | } else {
|
---|
510 | bgSize = bgSize.split(" ")[0];
|
---|
511 |
|
---|
512 | if (bgSize.endsWith("%")) {
|
---|
513 | bgWidth = (int) ((Integer.parseInt(bgSize.replaceAll("\\D", "")) / 100.0) * width);
|
---|
514 | } else if (bgSize.endsWith("px")) {
|
---|
515 | bgWidth = Integer.parseInt(bgSize.replaceAll("\\D", ""));
|
---|
516 | }
|
---|
517 | }
|
---|
518 |
|
---|
519 | try {
|
---|
520 | WebParser.addImageFromUrl(bgImage, linkUrl, frame, x, y, bgWidth, cropStartX, cropStartY, cropEndX, cropEndY, bgRepeat, originXPercent, originYPercent);
|
---|
521 | } catch (MalformedURLException mue) {
|
---|
522 | // probably a 'data:' url, not supported yet
|
---|
523 | mue.printStackTrace();
|
---|
524 | }
|
---|
525 | }
|
---|
526 |
|
---|
527 | String imgSrc;
|
---|
528 |
|
---|
529 | if (currentNode.getNodeName().toLowerCase().equals("img") && (imgSrc = JavaFX.JSObjectGetMember.invoke(currentNode, "src").toString()) != null) {
|
---|
530 | try {
|
---|
531 | WebParser.addImageFromUrl(imgSrc, linkUrl, frame, x, y, (int) width, null, null, null, null, null, 0, 0);
|
---|
532 | } catch (MalformedURLException mue) {
|
---|
533 | // probably a 'data:' url, not supported yet
|
---|
534 | mue.printStackTrace();
|
---|
535 | }
|
---|
536 | }
|
---|
537 | }
|
---|
538 | }
|
---|
539 | }
|
---|
540 | }
|
---|
541 |
|
---|
542 | } catch (Exception e) {
|
---|
543 | e.printStackTrace();
|
---|
544 | }
|
---|
545 | System.out.println("Parsed frame");
|
---|
546 | FrameUtils.Parse(frame);
|
---|
547 | frame.setChanged(true);
|
---|
548 | FrameIO.SaveFrame(frame);
|
---|
549 | }
|
---|
550 | });
|
---|
551 | } catch (Exception e) {
|
---|
552 | e.printStackTrace();
|
---|
553 | }
|
---|
554 | }
|
---|
555 |
|
---|
556 | /**
|
---|
557 | * @param rgbString
|
---|
558 | * string in the format <i>rgb(x,x,x)</i> or <i>rgba(x,x,x,x)</i>
|
---|
559 | * @return A Color object that should match the rgb string passed int. Returns null if alpha is 0
|
---|
560 | */
|
---|
561 | private static Color rgbStringToColor(String rgbString) {
|
---|
562 | // Splitting the string into 'rgb' and 'x, x, x'
|
---|
563 | String[] tmpStrings = rgbString.split("\\(|\\)");
|
---|
564 |
|
---|
565 | // Splitting up the RGB(A) components into an array
|
---|
566 | tmpStrings = tmpStrings[1].split(",");
|
---|
567 |
|
---|
568 | int[] components = new int[4];
|
---|
569 | Arrays.fill(components, 255);
|
---|
570 |
|
---|
571 | for (int i = 0; i < tmpStrings.length; i++) {
|
---|
572 | Float d = Float.parseFloat(tmpStrings[i].trim());
|
---|
573 |
|
---|
574 | components[i] = Math.round(d);
|
---|
575 | }
|
---|
576 |
|
---|
577 | if (components[3] > 0) {
|
---|
578 | return new Color(components[0], components[1], components[2], components[3]);
|
---|
579 | } else {
|
---|
580 | return null;
|
---|
581 | }
|
---|
582 | }
|
---|
583 |
|
---|
584 | private static boolean elementVisible(float x, float y, float width, float height, Object style) {
|
---|
585 | try {
|
---|
586 | if (width <= 0 || height <= 0 || x + width <= 0 || y + height <= 0 || ((String) JavaFX.JSObjectCall.invoke(style, "getPropertyValue", new Object[] { "visibility" })).equals("hidden")
|
---|
587 | || ((String) JavaFX.JSObjectCall.invoke(style, "getPropertyValue", new Object[] { "display" })).equals("none")) {
|
---|
588 | return false;
|
---|
589 | } else {
|
---|
590 | return true;
|
---|
591 | }
|
---|
592 | } catch (Exception e) {
|
---|
593 | e.printStackTrace();
|
---|
594 | return false;
|
---|
595 | }
|
---|
596 | }
|
---|
597 |
|
---|
598 | /**
|
---|
599 | * @param imgSrc
|
---|
600 | * URL of the image to add
|
---|
601 | * @param frame
|
---|
602 | * Frame to add the image to
|
---|
603 | * @param x
|
---|
604 | * X-coordinate at which the image should be placed on the frame
|
---|
605 | * @param y
|
---|
606 | * Y-coordinate at which the image should be placed on the frame
|
---|
607 | * @param width
|
---|
608 | * Width of the image once added to the frame. Negative 1 (-1) will cause the actual width of the image file to be used
|
---|
609 | *
|
---|
610 | * @param cropStartX
|
---|
611 | * X-coordinate at which to start crop, or null for no crop
|
---|
612 | * @param cropStartY
|
---|
613 | * Y-coordinate at which to start crop, or null for no crop
|
---|
614 | * @param cropEndX
|
---|
615 | * X-coordinate at which to end the crop, or null for no crop
|
---|
616 | * @param cropEndY
|
---|
617 | * Y-coordinate at which to end the crop, or null for no crop
|
---|
618 | *
|
---|
619 | * @param repeat
|
---|
620 | * String determining how the image should be tiled/repeated. Valid strings are: <i>no-repeat</i>, <i>repeat-x</i>, or
|
---|
621 | * <i>repeat-y</i>. All other values (including null) will cause the image to repeat in both directions
|
---|
622 | *
|
---|
623 | * @param originXPercent
|
---|
624 | * Percentage into the image to use as the x coordinate of the image's origin point
|
---|
625 | * @param originYPercent
|
---|
626 | * Percentage into the image to use as the y coordinate of the image's origin point
|
---|
627 | *
|
---|
628 | * @throws MalformedURLException
|
---|
629 | * @throws IOException
|
---|
630 | */
|
---|
631 | 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,
|
---|
632 | float originXPercent, float originYPercent)
|
---|
633 | throws MalformedURLException,
|
---|
634 | IOException {
|
---|
635 |
|
---|
636 | URL imgUrl = new URL(imgSrc);
|
---|
637 |
|
---|
638 | HttpURLConnection connection = (HttpURLConnection) (imgUrl.openConnection());
|
---|
639 |
|
---|
640 | // Spoofing a widely accepted User Agent, since some sites refuse to serve non-webbrowser clients
|
---|
641 | connection.setRequestProperty("User-Agent", "Mozilla/5.0");
|
---|
642 |
|
---|
643 | BufferedImage img = ImageIO.read(connection.getInputStream());
|
---|
644 |
|
---|
645 | int hashcode = Arrays.hashCode(img.getData().getPixels(0, 0, img.getWidth(), img.getHeight(), (int[]) null));
|
---|
646 | File out = new File(FrameIO.IMAGES_PATH + Integer.toHexString(hashcode) + ".png");
|
---|
647 | out.mkdirs();
|
---|
648 | ImageIO.write(img, "png", out);
|
---|
649 |
|
---|
650 | if (cropEndX == null || cropStartX == null || cropEndY == null || cropStartY == null) {
|
---|
651 | cropStartX = 0;
|
---|
652 | cropStartY = 0;
|
---|
653 | cropEndX = img.getWidth();
|
---|
654 | cropEndY = img.getHeight();
|
---|
655 | } else if (cropStartX < 0) {
|
---|
656 | cropEndX = cropEndX - cropStartX;
|
---|
657 | x = x + Math.abs(cropStartX);
|
---|
658 | cropStartX = 0;
|
---|
659 | }
|
---|
660 |
|
---|
661 | if (cropStartY < 0) {
|
---|
662 | cropEndY = cropEndY - cropStartY;
|
---|
663 | y = y + Math.abs(cropStartY);
|
---|
664 | cropStartY = 0;
|
---|
665 | }
|
---|
666 |
|
---|
667 | if (width < 0) {
|
---|
668 | width = img.getWidth();
|
---|
669 | }
|
---|
670 |
|
---|
671 | if (repeat != null) {
|
---|
672 | if (repeat.equals("no-repeat")) {
|
---|
673 | int tmpCropEndY = (int) (cropStartY + ((float) width / img.getWidth()) * img.getHeight());
|
---|
674 | int tmpCropEndX = cropStartX + width;
|
---|
675 |
|
---|
676 | cropEndX = (cropEndX < tmpCropEndX) ? cropEndX : tmpCropEndX;
|
---|
677 | cropEndY = (cropEndY < tmpCropEndY) ? cropEndY : tmpCropEndY;
|
---|
678 | } else if (repeat.equals("repeat-x")) {
|
---|
679 | int tmpCropEndY = (int) (cropStartY + ((float) width / img.getWidth()) * img.getHeight());
|
---|
680 | cropEndY = (cropEndY < tmpCropEndY) ? cropEndY : tmpCropEndY;
|
---|
681 | } else if (repeat.equals("repeat-y")) {
|
---|
682 | int tmpCropEndX = cropStartX + width;
|
---|
683 | cropEndX = (cropEndX < tmpCropEndX) ? cropEndX : tmpCropEndX;
|
---|
684 | }
|
---|
685 | }
|
---|
686 |
|
---|
687 | if (originXPercent > 0) {
|
---|
688 | int actualWidth = cropEndX - cropStartX;
|
---|
689 |
|
---|
690 | int originXPixels = Math.round(originXPercent * actualWidth);
|
---|
691 |
|
---|
692 | x = x - originXPixels;
|
---|
693 |
|
---|
694 | cropStartX = (int) (cropStartX + (width - actualWidth) * originXPercent);
|
---|
695 | cropEndX = (int) (cropEndX + (width - actualWidth) * originXPercent);
|
---|
696 | }
|
---|
697 |
|
---|
698 | if (originYPercent > 0) {
|
---|
699 | int height = (int) ((img.getHeight() / (float) img.getWidth()) * width);
|
---|
700 | int actualHeight = (cropEndY - cropStartY);
|
---|
701 | int originYPixels = Math.round(originYPercent * actualHeight);
|
---|
702 |
|
---|
703 | y = y - originYPixels;
|
---|
704 |
|
---|
705 | cropStartY = (int) (cropStartY + (height - actualHeight) * originYPercent);
|
---|
706 | cropEndY = (int) (cropEndY + (height - actualHeight) * originYPercent);
|
---|
707 | }
|
---|
708 |
|
---|
709 | Text text = new Text("@i: " + out.getName() + " " + width);
|
---|
710 | text.setPosition(x, y);
|
---|
711 |
|
---|
712 | Picture pic = ItemUtils.CreatePicture(text, frame);
|
---|
713 |
|
---|
714 | float invScale = 1 / pic.getScale();
|
---|
715 |
|
---|
716 | pic.setCrop((int)(cropStartX * invScale), (int)(cropStartY * invScale), (int)(cropEndX * invScale), (int)(cropEndY * invScale));
|
---|
717 |
|
---|
718 | if (linkUrl != null && !linkUrl.equals("undefined")) {
|
---|
719 | pic.setAction("goto " + linkUrl);
|
---|
720 | pic.setActionMark(false);
|
---|
721 | }
|
---|
722 |
|
---|
723 | frame.addItem(pic);
|
---|
724 | pic.anchor();
|
---|
725 | pic.getSource().anchor();
|
---|
726 | }
|
---|
727 |
|
---|
728 | private static String[] splitIntoWords(String toSplit) {
|
---|
729 | ArrayList<String> words = new ArrayList<String>();
|
---|
730 | Pattern regex = Pattern.compile("\\s+");
|
---|
731 | Matcher matcher = regex.matcher(toSplit);
|
---|
732 |
|
---|
733 | // The index at which the previous word ended
|
---|
734 | int prevEndIndex = 0;
|
---|
735 |
|
---|
736 | String prev = null;
|
---|
737 |
|
---|
738 | while (matcher.find()) {
|
---|
739 | String w = toSplit.substring(prevEndIndex, matcher.start());
|
---|
740 |
|
---|
741 | if (prev != null) {
|
---|
742 | words.add(prev + " ");
|
---|
743 | }
|
---|
744 |
|
---|
745 | prev = w;
|
---|
746 | prevEndIndex = matcher.end();
|
---|
747 | }
|
---|
748 |
|
---|
749 | // Adding the final two words
|
---|
750 | if (prev != null) {
|
---|
751 | words.add(prev + " ");
|
---|
752 | }
|
---|
753 |
|
---|
754 | words.add(toSplit.substring(prevEndIndex));
|
---|
755 |
|
---|
756 | return words.toArray(new String[words.size()]);
|
---|
757 | }
|
---|
758 | }
|
---|