source: trunk/src/org/expeditee/io/WebParser.java@ 884

Last change on this file since 884 was 884, checked in by ngw8, 10 years ago

Actually fixed webparser bug for pages with no titles

File size: 53.8 KB
Line 
1package org.expeditee.io;
2
3import java.awt.Color;
4import java.awt.Font;
5import java.awt.image.BufferedImage;
6import java.io.File;
7import java.io.IOException;
8import java.lang.reflect.InvocationTargetException;
9import java.net.HttpURLConnection;
10import java.net.MalformedURLException;
11import java.net.URL;
12import java.text.SimpleDateFormat;
13/*
14 * 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.
15 * 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),
16 * or you can just exclude JfxBrowser, WebParser and JfxbrowserActions from the build path.
17 *
18 * If you are using Ant to build/run, 'ant build' will try to build with JavaFX jar added to the classpath.
19 * If this fails, 'ant build-nojfx' will build with the JfxBrowser, WebParser and JfxbrowserActions excluded from the build path.
20 */
21import java.util.Arrays;
22import java.util.Date;
23
24import javafx.animation.AnimationTimer;
25import javafx.application.Platform;
26import javafx.beans.value.ChangeListener;
27import javafx.beans.value.ObservableValue;
28import javafx.concurrent.Worker.State;
29import javafx.embed.swing.SwingFXUtils;
30import javafx.scene.SnapshotParameters;
31import javafx.scene.image.WritableImage;
32import javafx.scene.web.WebEngine;
33import javafx.scene.web.WebView;
34
35import javax.imageio.ImageIO;
36
37import netscape.javascript.JSObject;
38
39import org.expeditee.gui.DisplayIO;
40import org.expeditee.gui.Frame;
41import org.expeditee.gui.FrameGraphics;
42import org.expeditee.gui.FrameIO;
43import org.expeditee.gui.FrameMouseActions;
44import org.expeditee.gui.FrameUtils;
45import org.expeditee.gui.MessageBay;
46import org.expeditee.gui.MessageBay.Progress;
47import org.expeditee.items.ItemUtils;
48import org.expeditee.items.Justification;
49import org.expeditee.items.Picture;
50import org.expeditee.items.Text;
51import org.expeditee.items.widgets.JfxBrowser;
52import org.w3c.dom.Node;
53
54/**
55 * Methods to convert webpages to Expeditee frames
56 *
57 * @author ngw8
58 * @author jts21
59 */
60public class WebParser {
61
62
63 /**
64 * Loads a webpage and renders it as Expeditee frame(s)
65 *
66 * @param URL
67 * Page to load
68 * @param frame
69 * The Expeditee frame to output the converted page to
70 */
71 public static void parseURL(final String URL, final Frame frame) {
72 try {
73 Platform.runLater(new Runnable() {
74 @Override
75 public void run() {
76 try {
77 WebEngine webEngine = new WebEngine(URL);
78 loadPage(webEngine, frame);
79 } catch (Exception e) {
80 e.printStackTrace();
81 }
82 }
83 });
84 } catch (Exception e) {
85 e.printStackTrace();
86 }
87 }
88
89 protected static void loadPage(final WebEngine webEngine, final Frame frame) throws Exception {
90 webEngine.getLoadWorker().stateProperty().addListener(new ChangeListener<State>() {
91
92 @Override
93 public void changed(ObservableValue<? extends State> ov, State oldState, State newState) {
94
95 switch (newState) {
96 case READY: // READY
97 // MessageBay.displayMessage("WebEngine ready");
98 break;
99 case SCHEDULED: // SCHEDULED
100 // MessageBay.displayMessage("Scheduled page load");
101 break;
102 case RUNNING: // RUNNING
103 System.out.println("Loading page!");
104 // MessageBay.displayMessage("WebEngine running");
105 break;
106 case SUCCEEDED: // SUCCEEDED
107 // MessageBay.displayMessage("Finished loading page");
108 System.out.println("Parsing page!");
109 webEngine.executeScript("window.resizeTo(800, 800);"
110 + "document.body.style.width = '1000px'");
111 parsePage(webEngine, frame);
112 System.out.println("Parsed page!");
113 break;
114 case CANCELLED: // CANCELLED
115 MessageBay.displayMessage("Cancelled loading page");
116 break;
117 case FAILED: // FAILED
118 MessageBay.displayMessage("Failed to load page");
119 break;
120 }
121 }
122 });
123 }
124
125 /**
126 * Converts a loaded page to Expeditee frame(s)
127 *
128 * @param webEngine
129 * The JavaFX WebEngine in which the page to be converted is loaded
130 * @param frame
131 * The Expeditee frame to output the converted page to
132 */
133 public static void parsePage(final WebEngine webEngine, final Frame frame) {
134 try {
135 Platform.runLater(new Runnable() {
136 @Override
137 public void run() {
138 try {
139 Progress progressBar = MessageBay.displayProgress("Converting web page");
140
141 Node doc = (Node) webEngine.executeScript("document.body");
142
143 JSObject window = (JSObject) webEngine.executeScript("window");
144
145 frame.setBackgroundColor(rgbStringToColor((String) ((JSObject) (window.call("getComputedStyle", new Object[] { doc }))).call("getPropertyValue",
146 new Object[] { "background-color" })));
147
148 // Functions to be used later in JavaScript
149 webEngine.executeScript(""
150 + "function addToSpan(text) {"
151 + " span = document.createElement('wordSpan');"
152 + " span.textContent = text;"
153 + " par.insertBefore(span, refNode);"
154 // Checking if the current word is on a new line (i.e. lower than the previous word)
155 + " if (prevSpan !== null && span.getBoundingClientRect().top > prevSpan.getBoundingClientRect().top) {"
156 // If it is, prepend a new line character to it. The new line characters doesn't affect the rendered HTML
157 + " span.textContent = '\\n' + span.textContent;"
158
159 // Checking if the previous word is horizontally aligned with the one before it.
160 // If it is, merge the text of the two spans
161 + " if ( prevPrevSpan !== null && prevPrevSpan.getBoundingClientRect().left == prevSpan.getBoundingClientRect().left) {"
162 + " prevPrevSpan.textContent = prevPrevSpan.textContent + prevSpan.textContent;"
163 + " par.removeChild(prevSpan);"
164 + " } else {"
165 + " prevPrevSpan = prevSpan;"
166 + " }"
167 + " prevSpan = span;"
168 + " } else if ( prevSpan !== null) {"
169 // Word is on the same line as the previous one, so merge the second into the span of the first
170 + " prevSpan.textContent = prevSpan.textContent + span.textContent;"
171 + " par.removeChild(span);"
172 + " } else {"
173 + " prevSpan = span;"
174 + " }"
175 + "}"
176
177 + "function splitIntoWords(toSplit) {"
178 + " var words = [];"
179 + " var pattern = /\\s+/g;"
180 + " var words = toSplit.split(pattern);"
181 + ""
182 + " for (var i = 0; i < words.length - 1; i++) {"
183 + " words[i] = words[i] + ' ';"
184 + " }"
185 + " return words;"
186 + "}"
187 );
188
189 // Using Javascript to get an array of all the text nodes in the document so they can be wrapped in spans. Have to
190 // loop through twice (once to build the array and once actually going through the array, otherwise when the
191 // textnode is removed from the document items end up being skipped)
192 JSObject textNodes = (JSObject) webEngine.executeScript(""
193 + "function getTextNodes(rootNode){"
194 + "var node;"
195 + "var textNodes=[];"
196 + "var walk = document.createTreeWalker(rootNode, NodeFilter.SHOW_TEXT);"
197 + "while(node=walk.nextNode()) {"
198 + "if((node.textContent.trim().length > 0)) { "
199 + "textNodes.push(node);"
200 + "}"
201 + "}"
202 + "return textNodes;"
203 + "}; "
204 + "getTextNodes(document.body)"
205 );
206
207 int nodesLength = (Integer) textNodes.getMember("length");
208
209 // Looping through all the text nodes in the document
210 for (int j = 0; j < nodesLength; j++) {
211 Node currentNode = (Node) textNodes.getSlot(j);
212
213 // Making the current node accessible in JavaScript
214 window.setMember("currentNode", currentNode);
215
216 webEngine.executeScript(""
217 + "var span = null, prevSpan = null, prevPrevSpan = null;"
218
219 // Removing repeated whitespace from the text node's content then splitting it into individual words
220 + "var textContent = currentNode.textContent.replace(/\\n|\\r/g, '').replace(/\\s+/g, ' ');"
221 + "var words = splitIntoWords(textContent);"
222
223 + "var refNode = currentNode.nextSibling;"
224 + "var par = currentNode.parentElement;"
225 + "currentNode.parentElement.removeChild(currentNode);"
226
227 + "for (var i = 0; i < words.length; i++) {"
228 + " addToSpan(words[i]);"
229 + "}"
230
231 + "if (prevPrevSpan !== null && prevPrevSpan.getBoundingClientRect().left == prevSpan.getBoundingClientRect().left) {"
232 + " prevPrevSpan.textContent = prevPrevSpan.textContent + prevSpan.textContent;"
233 + " par.removeChild(prevSpan);"
234 + "}"
235 );
236
237 // Will never reach 100% here, as the processing is not quite finished - progress is set to 100% at the end of
238 // the addPageToFrame loop below
239 progressBar.set((100 * (j)) / nodesLength);
240 }
241
242 // Finding all links within the page, then setting the href attribute of all their descendants to be the same
243 // link/URL.
244 // This is needed because there is no apparent and efficient way to check if an element is a child of a link when
245 // running through the document when added each element to Expeditee
246 webEngine.executeScript(""
247 + "var anchors = document.getElementsByTagName('a');"
248 + ""
249 + "for (var i = 0; i < anchors.length; i++) {"
250 + "var currentAnchor = anchors.item(i);"
251 + "var anchorDescendants = currentAnchor.querySelectorAll('*');"
252 + "for (var j = 0; j < anchorDescendants.length; j++) {"
253 + "anchorDescendants.item(j).href = currentAnchor.href;"
254 + "}"
255 + "}"
256 );
257
258 WebParser.addPageToFrame(doc, window, webEngine, frame);
259
260 progressBar.set(100);
261
262 } catch (Exception e) {
263 e.printStackTrace();
264 }
265 System.out.println("Parsed frame");
266 FrameUtils.Parse(frame);
267 frame.setChanged(true);
268 FrameIO.SaveFrame(frame);
269 }
270 });
271 } catch (Exception e) {
272 e.printStackTrace();
273 }
274 }
275
276 /**
277 * Converts a loaded page to Expeditee frame(s)
278 *
279 * @param webEngine
280 * The JavaFX WebEngine in which the page to be converted is loaded
281 * @param frame
282 * The Expeditee frame to output the converted page to
283 */
284 public static void parsePageSimple(final JfxBrowser browserWidget, final WebEngine webEngine, final WebView webView, final Frame frame) {
285 try {
286 Platform.runLater(new Runnable() {
287
288 @Override
289 public void run() {
290 browserWidget.setOverlayVisible(true);
291 browserWidget.setWebViewSize(FrameGraphics.getMaxFrameSize().getWidth() * 0.9, FrameGraphics.getMaxFrameSize().getHeight() * 0.9);
292 browserWidget.setScrollbarsVisible(false);
293 }
294 });
295
296 final Object notifier = new Object();
297
298 final MutableBool bottomReached = new MutableBool(false);
299 final MutableBool rightReached = new MutableBool(false);
300
301 final MutableInt verticalCount = new MutableInt(0);
302 final MutableInt horizontalCount = new MutableInt(0);
303
304 final String pageTitle;
305
306 if (webEngine.getTitle() != null) {
307 pageTitle = webEngine.getTitle();
308 } else {
309 pageTitle = "Untitled Page";
310 }
311
312 final Progress progressBar = MessageBay.displayProgress("Converting web page");
313
314 final Frame frameset = FrameIO.CreateNewFrameset(FrameIO.ConvertToValidFramesetName((new SimpleDateFormat("yy-MM-dd-HH-mm-ss").format(new Date())) + pageTitle));
315
316 frameset.setTitle(pageTitle);
317 frameset.getTitleItem().setSize(14);
318
319 WebParser.addButton("Return to original frame", frame.getName(), null, 200, frameset, null, 0f, 10f, null);
320
321
322 Text link = DisplayIO.getCurrentFrame().addText(FrameMouseActions.getX(), FrameMouseActions.getY(), pageTitle, null);
323 link.setLink(frameset.getName());
324
325 FrameMouseActions.pickup(link);
326
327 // Timer that fires every time JFX is redrawn. After a few redraws, the handle method of this takes a screenshot of the page,
328 // adds it to the frame, then adds the text on top
329 AnimationTimer timer = new AnimationTimer() {
330
331 int frameCount = 0;
332
333 Frame frameToAddTo = frameset;
334 int thumbWidth = 100;
335
336 @Override
337 public void handle(long arg0) {
338 // Must wait 2 frames before taking a snapshot of the webview, otherwise JavaFX won't have redrawn
339 if (frameCount++ > 1) {
340 frameCount = 0;
341 this.stop();
342
343 verticalCount.setValue(verticalCount.getValue() + 1);
344
345 frameToAddTo = FrameIO.CreateFrame(frameToAddTo.getFramesetName(), pageTitle, null);
346 frameToAddTo.removeAllItems(frameToAddTo.getItems());
347
348 try {
349 // removing the CSS that hides the text (otherwise the text would not pass the visibility check that is run on
350 // it before adding it to the frame)
351 webEngine.executeScript("cssHide.innerHTML = '';");
352
353 JSObject window = (JSObject) webEngine.executeScript("window");
354
355 int visibleWidth = (Integer) webEngine.executeScript("window.innerWidth");
356 int visibleHeight = (Integer) webEngine.executeScript("window.innerHeight");
357
358 WebParser.addTextToFrame(visibleWidth, visibleHeight, window, webEngine, frameToAddTo);
359
360 FrameIO.SaveFrame(frameToAddTo);
361 } catch (Exception ex) {
362 ex.printStackTrace();
363 }
364
365 webEngine.executeScript(""
366 // Setting all text to be hidden before taking the screenshot
367 + "var style = 'WordSpan { visibility: hidden !important;}';"
368 + "cssHide.appendChild(document.createTextNode(style));");
369
370 WritableImage img = new WritableImage((int)webView.getWidth(), (int)webView.getHeight());
371
372 webView.snapshot(new SnapshotParameters(), img);
373
374 // Getting a BufferedImage from the JavaFX image
375 BufferedImage image = SwingFXUtils.fromFXImage(img, null);
376
377 try {
378 int hashcode = Arrays.hashCode(image.getData().getPixels(0, 0, image.getWidth(), image.getHeight(), (int[]) null));
379
380 File out = new File(FrameIO.IMAGES_PATH + "webpage-" + Integer.toHexString(hashcode) + ".png");
381 out.mkdirs();
382 ImageIO.write(image, "png", out);
383
384 // Adding the image to the frame
385 frameToAddTo.addText(0, 0, "@i: " + out.getName(), null);
386
387 // Adding thumbnail to the overview page
388 Text thumb = frameset.addText((int) (thumbWidth * 1.1 * horizontalCount.getValue()) + 10,
389 (int) ((((float) thumbWidth / image.getWidth()) * image.getHeight()) * 1.1 * verticalCount.getValue()),
390 "@i: " + out.getName() + " " + thumbWidth,
391 null);
392
393 thumb.setLink(frameToAddTo.getName());
394 thumb.setBorderColor(Color.lightGray);
395 thumb.setThickness(1);
396
397 // Button to go to the next frame/page
398 WebParser.addButton("Next", null, "next", 70, frameToAddTo, null, 0f, 10f, null);
399
400 // Button to go to the previous frame/page
401 if (verticalCount.getValue() > 1 || horizontalCount.getValue() > 0) {
402 WebParser.addButton("Previous", null, "previous", 70, frameToAddTo, null, 85f, 10f, null);
403 }
404
405 // Button to return to the index/overview page
406 WebParser.addButton("Index", frameset.getName(), null, 70, frameToAddTo, null, null, 10f, 5f);
407
408 FrameIO.SaveFrame(frameToAddTo);
409 FrameIO.SaveFrame(frameset);
410
411 } catch (IOException e) {
412 e.printStackTrace();
413 }
414
415 image.flush();
416
417 synchronized (notifier) {
418 // Notifying that the timer has finished
419 notifier.notify();
420 }
421 }
422 }
423 };
424
425 Platform.runLater(new Runnable() {
426 @Override
427 public void run() {
428 try {
429 JSObject window = (JSObject) webEngine.executeScript("window");
430
431 webEngine.executeScript(""
432 // Initializing the counter used when scrolling the page
433 + "var scrollCounter = 0;"
434 + "var scrollCounterHorizontal = 0;");
435
436 // Setting up the element that contains the CSS to hide all text.
437 // This is wiped before the text is converted, then re-added before taking the screenshot
438 webEngine.executeScript(""
439 + "var cssHide = document.createElement('style');"
440 + "cssHide.type = 'text/css';"
441 + "var style = 'WordSpan { visibility: hidden !important;}';"
442 + "cssHide.appendChild(document.createTextNode(style));"
443 + "document.getElementsByTagName('head')[0].appendChild(cssHide);");
444
445 // Functions to be used later in JavaScript
446 webEngine.executeScript(""
447 + "function addToSpan(text) {"
448 + " span = document.createElement('wordSpan');"
449 + " span.textContent = text;"
450 + " par.insertBefore(span, refNode);"
451 + " if (prevSpan !== null && span.getBoundingClientRect().top > prevSpan.getBoundingClientRect().top) {"
452 + " span.textContent = '\\n' + span.textContent;"
453 + " if ( prevPrevSpan !== null && prevPrevSpan.getBoundingClientRect().left == prevSpan.getBoundingClientRect().left) {"
454 + " prevPrevSpan.textContent = prevPrevSpan.textContent + prevSpan.textContent;"
455 + " par.removeChild(prevSpan);"
456 + " } else {"
457 + " prevPrevSpan = prevSpan;"
458 + " }"
459 + " prevSpan = span;"
460 + " } else if ( prevSpan !== null) {"
461 + " prevSpan.textContent = prevSpan.textContent + span.textContent;"
462 + " par.removeChild(span);"
463 + " } else {"
464 + " prevSpan = span;"
465 + " }"
466 + "}"
467
468 + "function splitIntoWords(toSplit) {"
469 + " var words = [];"
470 + " var pattern = /\\s+/g;"
471 + " var words = toSplit.split(pattern);"
472 + ""
473 + " for (var i = 0; i < words.length - 1; i++) {"
474 + " words[i] = words[i] + ' ';"
475 + " }"
476 + " return words;"
477 + "}"
478 );
479
480 // Using Javascript to get an array of all the text nodes in the document so they can be wrapped in spans. Have to
481 // loop through twice (once to build the array and once actually going through the array, otherwise when the
482 // textnode is removed from the document items end up being skipped)
483 JSObject textNodes = (JSObject) webEngine.executeScript(""
484 + "function getTextNodes(rootNode){"
485 + "var node;"
486 + "var textNodes=[];"
487 + "var walk = document.createTreeWalker(rootNode, NodeFilter.SHOW_TEXT);"
488 + "while(node=walk.nextNode()) {"
489 + "if((node.textContent.trim().length > 0)) { "
490 + "textNodes.push(node);"
491 + "}"
492 + "}"
493 + "return textNodes;"
494 + "}; "
495 + "getTextNodes(document.body)"
496 );
497
498 int nodesLength = (Integer) textNodes.getMember("length");
499
500 // Looping through all the text nodes in the document
501 for (int j = 0; j < nodesLength; j++) {
502 Node currentNode = (Node) textNodes.getSlot(j);
503
504 // Making the current node accessible in JavaScript
505 window.setMember("currentNode", currentNode);
506
507 webEngine.executeScript(""
508 + "var span = null, prevSpan = null, prevPrevSpan = null;"
509
510 // Removing repeated whitespace from the text node's content then splitting it into individual words
511 + "var textContent = currentNode.textContent.replace(/\\n|\\r/g, '').replace(/\\s+/g, ' ');"
512 + "var words = splitIntoWords(textContent);"
513
514 + "var refNode = currentNode.nextSibling;"
515 + "var par = currentNode.parentElement;"
516 + "currentNode.parentElement.removeChild(currentNode);"
517
518 + "for (var i = 0; i < words.length; i++) {"
519 + " addToSpan(words[i]);"
520 + "}"
521
522 + "if (prevPrevSpan !== null && prevPrevSpan.getBoundingClientRect().left == prevSpan.getBoundingClientRect().left) {"
523 + " prevPrevSpan.textContent = prevPrevSpan.textContent + prevSpan.textContent;"
524 + " par.removeChild(prevSpan);"
525 + "}"
526 );
527
528 // Will never reach 100% here, as the processing is not quite finished - progress is set to 100% at the end of
529 // the addPageToFrame loop below
530 progressBar.set((50 * (j + 1)) / nodesLength);
531 }
532
533 // Finding all links within the page, then setting the href attribute of all their descendants to be the same
534 // link/URL.
535 // This is needed because there is no apparent and efficient way to check if an element is a child of a link when
536 // running through the document when added each element to Expeditee
537 webEngine.executeScript(""
538 + "var anchors = document.getElementsByTagName('a');"
539 + ""
540 + "for (var i = 0; i < anchors.length; i++) {"
541 + "var currentAnchor = anchors.item(i);"
542 + "var anchorDescendants = currentAnchor.querySelectorAll('*');"
543 + "for (var j = 0; j < anchorDescendants.length; j++) {"
544 + "anchorDescendants.item(j).href = currentAnchor.href;"
545 + "}"
546 + "}"
547 );
548
549 } catch (Exception ex) {
550 ex.printStackTrace();
551 }
552
553 synchronized (notifier) {
554 notifier.notify();
555 }
556 }
557 });
558
559 synchronized (notifier) {
560 try {
561 // Waiting for the page setup (splitting into spans) to finish
562 notifier.wait();
563 } catch (InterruptedException e) {
564 // TODO Auto-generated catch block
565 e.printStackTrace();
566 }
567 }
568
569 // Loop that scrolls the page horizontally
570 while (!rightReached.getValue()) {
571 bottomReached.setValue(false);
572
573 Platform.runLater(new Runnable() {
574 @Override
575 public void run() {
576 try {
577 // Scrolling down the page
578 webEngine.executeScript(""
579 + "scrollCounter = 0;"
580 + "window.scrollTo(scrollCounterHorizontal * 0.9 * window.innerWidth, 0);"
581 + "scrollCounterHorizontal = scrollCounterHorizontal+1;");
582
583 rightReached.setValue((Boolean) webEngine.executeScript("(window.pageXOffset + window.innerWidth >= document.documentElement.scrollWidth)"));
584
585 } catch (Exception e) {
586 e.printStackTrace();
587 }
588 }
589 });
590
591 // Loop that scrolls the page vertically (for each horizontal scroll position)
592 while (!bottomReached.getValue()) {
593 Platform.runLater(new Runnable() {
594 @Override
595 public void run() {
596 try {
597 // Scrolling down the page
598 webEngine.executeScript(""
599 + "window.scrollTo(window.pageXOffset, scrollCounter * 0.9 * window.innerHeight);"
600 + "scrollCounter = scrollCounter+1;");
601
602 bottomReached.setValue((Boolean) webEngine.executeScript("(window.pageYOffset + window.innerHeight >= document.documentElement.scrollHeight)"));
603
604 synchronized (notifier) {
605 notifier.notify();
606 }
607
608 } catch (Exception e) {
609 e.printStackTrace();
610 }
611 }
612 });
613
614 synchronized (notifier) {
615 try {
616 // Waiting for the page to be scrolled
617 notifier.wait();
618 } catch (InterruptedException e) {
619 // TODO Auto-generated catch block
620 e.printStackTrace();
621 }
622 }
623
624 timer.start();
625
626 synchronized (notifier) {
627 try {
628 // Waiting for the timer thread to finish before looping again
629 notifier.wait();
630 } catch (InterruptedException e) {
631 // TODO Auto-generated catch block
632 e.printStackTrace();
633 }
634 }
635
636 }
637
638 horizontalCount.setValue(horizontalCount.getValue() + 1);
639 verticalCount.setValue(0);
640 }
641
642 progressBar.set(100);
643
644 Platform.runLater(new Runnable() {
645 @Override
646 public void run() {
647 // Reloading the page once the parsing is done - only realistic way to reset (i.e. remove all the added WordSpan tags)
648 // the page
649 webEngine.reload();
650 }
651 });
652
653 } catch (Exception ex) {
654 ex.printStackTrace();
655 }
656
657 Platform.runLater(new Runnable() {
658
659 @Override
660 public void run() {
661 browserWidget.setOverlayVisible(false);
662 browserWidget.rebindWebViewSize();
663 browserWidget.setScrollbarsVisible(true);
664 }
665 });
666
667 }
668
669 /**
670 * @param rgbString
671 * string in the format <i>rgb(x,x,x)</i> or <i>rgba(x,x,x,x)</i>
672 * @return A Color object that should match the rgb string passed int. Returns null if alpha is 0
673 */
674 private static Color rgbStringToColor(String rgbString) {
675
676 if (rgbString == null) {
677 return null;
678 }
679
680 // Splitting the string into 'rgb' and 'x, x, x'
681 String[] tmpStrings = rgbString.split("\\(|\\)");
682
683 // Splitting up the RGB(A) components into an array
684 tmpStrings = tmpStrings[1].split(",");
685
686 int[] components = new int[4];
687 Arrays.fill(components, 255);
688
689 for (int i = 0; i < tmpStrings.length; i++) {
690 Float d = Float.parseFloat(tmpStrings[i].trim());
691
692 components[i] = Math.round(d);
693 }
694
695 if (components[3] > 0) {
696 return new Color(components[0], components[1], components[2], components[3]);
697 } else {
698 return null;
699 }
700 }
701
702 /**
703 * @param rootElement
704 * Element that will be converted (including all sub-elements)
705 * @param backgroundColor
706 * 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)"
707 * @param window
708 * 'window' from Javascript
709 * @param webEngine
710 * Web engine that the page is loaded in
711 * @param frame
712 * Expeditee frame to add the converted page to
713 * @throws IllegalArgumentException
714 * @throws IllegalAccessException
715 */
716 private static void addPageToFrame(Node rootElement, JSObject window, WebEngine webEngine, Frame frame) throws InvocationTargetException, IllegalAccessException,
717 IllegalArgumentException {
718
719 Node currentNode = rootElement;
720
721 if (currentNode.getNodeType() == Node.TEXT_NODE || currentNode.getNodeType() == Node.ELEMENT_NODE) {
722
723 JSObject style;
724 JSObject bounds;
725
726 if (currentNode.getNodeType() == Node.TEXT_NODE) {
727 // CSS style for the element
728 style = (JSObject) window.call("getComputedStyle", new Object[] { currentNode.getParentNode() });
729
730 // Getting a rectangle that represents the area and position of the element
731 bounds = (JSObject) ((JSObject) currentNode.getParentNode()).call("getBoundingClientRect", new Object[] {});
732 } else {
733 style = (JSObject) window.call("getComputedStyle", new Object[] { currentNode });
734
735 bounds = (JSObject) ((JSObject) currentNode).call("getBoundingClientRect", new Object[] {});
736 }
737
738 // Bounding rectangle position is relative to the current view, so scroll position must be added to x/y
739 // TODO: This doesn't check if an element or any of its parent elements have position:fixed set - the only
740 // way to check seems to be to walking through the element's parents until the document root is reached
741 float x = Float.valueOf(bounds.getMember("left").toString()) + Float.valueOf(webEngine.executeScript("window.pageXOffset").toString());
742 float y = Float.valueOf(bounds.getMember("top").toString()) + Float.valueOf(webEngine.executeScript("window.pageYOffset").toString());
743
744 float width = Float.valueOf(bounds.getMember("width").toString());
745 float height = Float.valueOf(bounds.getMember("height").toString());
746
747 // Checking if the element is actually visible on the page
748 if (WebParser.elementVisible(x, y, width, height, style)) {
749
750 // Filtering the node type, starting with text nodes
751 if (currentNode.getNodeType() == Node.TEXT_NODE) {
752
753 String fontSize = ((String) style.call("getPropertyValue", new Object[] { "font-size" }));
754
755 // Trimming off the units (always px) from the font size
756 fontSize = fontSize.substring(0, fontSize.length() - 2);
757
758 // Always returns in format "rgb(x,x,x)" or "rgba(x,x,x,x)"
759 String color = (String) style.call("getPropertyValue", new Object[] { "color" });
760
761 // Always returns in format "rgb(x,x,x)" or "rgba(x,x,x,x)"
762 String bgColorString = (String) style.call("getPropertyValue", new Object[] { "background-color" });
763
764 String align = (String) style.call("getPropertyValue", new Object[] { "text-align" });
765
766 // Returns comma-separated list of typefaces
767 String typeface = (String) style.call("getPropertyValue", new Object[] { "font-family" });
768
769 String[] typefaces = typeface.split(", |,");
770
771 String weight = (String) style.call("getPropertyValue", new Object[] { "font-weight" });
772
773 String fontStyle = (String) style.call("getPropertyValue", new Object[] { "font-style" });
774
775 // Returns "normal" or a value in pixels (e.g. "10px")
776 String letterSpacing = (String) style.call("getPropertyValue", new Object[] { "letter-spacing" });
777
778 // Returns a value in pixels (e.g. "10px")
779 String lineHeight = (String) style.call("getPropertyValue", new Object[] { "line-height" });
780
781 String textTransform = (String) style.call("getPropertyValue", new Object[] { "text-transform" });
782
783 String linkUrl = (String) ((JSObject) currentNode.getParentNode()).getMember("href");
784
785 Boolean fontFound = false;
786 Font font = new Font(null);
787
788 // Looping through all font-families listed in the element's CSS until one that is installed is
789 // found, or the end of the list is reached, in which case the default font is used
790 for (int j = 0; j < typefaces.length && !fontFound; j++) {
791 if (typefaces[j].toLowerCase().equals("sans-serif")) {
792 typefaces[j] = "Arial Unicode MS";
793 } else if (typefaces[j].toLowerCase().equals("serif")) {
794 typefaces[j] = "Times New Roman";
795 } else if ((typefaces[j].toLowerCase().equals("arial"))) {
796 // Have to use Arial Unicode, otherwise unicode characters display incorrectly
797 typefaces[j] = "Arial Unicode MS";
798 }
799
800 // Regex will remove any inverted commas surrounding multi-word typeface names
801 font = new Font(typefaces[j].replaceAll("^'|'$", ""), Font.PLAIN, 12);
802
803 // If the font isn't found, Java just uses Font.DIALOG, so this check checks whether the font was found
804 if (!(font.getFamily().toLowerCase().equals(Font.DIALOG.toLowerCase()))) {
805 fontFound = true;
806 }
807 }
808
809 if (font.getFamily().toLowerCase().equals(Font.DIALOG.toLowerCase())) {
810 font = new Font("Times New Roman", Font.PLAIN, 12);
811 }
812
813 String fontStyleComplete = "";
814
815 int weightInt = 0;
816
817 try {
818 weightInt = Integer.parseInt(weight);
819 } catch (NumberFormatException nfe) {
820 // Use default value as set above
821 }
822
823 // checking if font is bold - i.e. 'bold', 'bolder' or weight over 500
824 if (weight.toLowerCase().startsWith("bold") || weightInt > 500) {
825 fontStyleComplete = fontStyleComplete.concat("bold");
826 }
827
828 if (fontStyle.toLowerCase().equals("italic") || fontStyle.toLowerCase().equals("oblique")) {
829 fontStyleComplete = fontStyleComplete.concat("italic");
830 }
831
832 float fontSizeFloat = 12;
833
834 try {
835 fontSizeFloat = Float.valueOf(fontSize);
836 } catch (NumberFormatException nfe) {
837 // Use default value as set above
838 }
839
840 float letterSpacingFloat = -0.008f;
841
842 try {
843 letterSpacingFloat = (Integer.parseInt(letterSpacing.substring(0, letterSpacing.length() - 2)) / (fontSizeFloat));
844 } catch (NumberFormatException nfe) {
845 // Use default value as set above
846 }
847
848 float lineHeightInt = -1;
849
850 try {
851 lineHeightInt = (Float.parseFloat(lineHeight.substring(0, lineHeight.length() - 2)));
852 } catch (NumberFormatException nfe) {
853 // Use default value as set above
854 }
855
856 Text t;
857
858 String textContent = currentNode.getTextContent().replaceAll("[^\\S\\n]+", " ");
859 textContent = textContent.replaceAll("^(\\s)(\\n|\\r)", "");
860
861 if (textTransform.equals("uppercase")) {
862 textContent = textContent.toUpperCase();
863 } else if (textTransform.equals("lowercase")) {
864 textContent = textContent.toUpperCase();
865 }
866
867 // Adding the text to the frame. Expeditee text seems to be positioned relative to the baseline of the first line, so
868 // the font size has to be added to the y-position
869 t = frame.addText(Math.round(x), Math.round(y + fontSizeFloat), textContent, null);
870
871 t.setColor(rgbStringToColor(color));
872 t.setBackgroundColor(rgbStringToColor(bgColorString));
873 t.setFont(font);
874 t.setSize(fontSizeFloat);
875 t.setFontStyle(fontStyleComplete);
876 t.setLetterSpacing(letterSpacingFloat);
877
878 // Removing any spacing between lines allowing t.getLineHeight() to be used to get the actual height
879 // of just the characters (i.e. distance from ascenders to descenders)
880 t.setSpacing(0);
881
882 t.setSpacing(lineHeightInt - t.getLineHeight());
883
884 if (align.equals("left")) {
885 t.setJustification(Justification.left);
886 } else if (align.equals("right")) {
887 t.setJustification(Justification.right);
888 } else if (align.equals("center")) {
889 t.setJustification(Justification.center);
890 } else if (align.equals("justify")) {
891 t.setJustification(Justification.full);
892 }
893
894 // Font size is added to the item width to give a little breathing room
895 t.setWidth(Math.round(width + (t.getSize())));
896
897 if (!linkUrl.equals("undefined")) {
898 t.setAction("gotourl " + linkUrl);
899 t.setActionMark(false);
900 }
901
902 } else if (currentNode.getNodeType() == Node.ELEMENT_NODE) {
903
904 // Always returns in format "rgb(x,x,x)" or "rgba(x,x,x,x)"
905 String bgColorString = (String) style.call("getPropertyValue", new Object[] { "background-color" });
906
907 Color bgColor = rgbStringToColor(bgColorString);
908
909 // If the element has a background color then add it (to Expeditee) as a rectangle with that background color
910 if (bgColor != null) {
911 frame.addRectangle(Math.round(x), Math.round(y), Math.round(width), Math.round(height), 0, null, bgColor);
912 }
913
914 String linkUrl = (String) ((JSObject) currentNode).getMember("href");
915
916 // background image, returns in format "url(protocol://absolute/path/to/img.extension)" for images,
917 // may also return gradients, data, etc. (not handled yet). Only need to add bg image on
918 // 'ELEMENT_NODE' (and not 'TEXT_NODE' otherwise there would be double-ups
919 if (((String) style.call("getPropertyValue", new Object[] { "background-image" })).startsWith("url(")) {
920
921 try {
922 WebParser.addBackgroundImageFromNode(currentNode, style, frame, linkUrl, x, y, width, height);
923 } catch (MalformedURLException mue) {
924 // probably a 'data:' url, not supported yet
925 mue.printStackTrace();
926 } catch (IOException e) {
927 // TODO Auto-generated catch block
928 e.printStackTrace();
929 }
930 }
931
932 String imgSrc;
933
934 if (currentNode.getNodeName().toLowerCase().equals("img") && (imgSrc = ((JSObject) currentNode).getMember("src").toString()) != null) {
935 try {
936 WebParser.addImageFromUrl(imgSrc, linkUrl, frame, x, y, (int) width, null, null, null, null, null, 0, 0);
937 } catch (MalformedURLException mue) {
938 // probably a 'data:' url, not supported yet
939 mue.printStackTrace();
940 } catch (IOException e) {
941 // TODO Auto-generated catch block
942 e.printStackTrace();
943 }
944 }
945 }
946 }
947
948 Node childNode = currentNode.getFirstChild();
949
950 while (childNode != null) {
951 addPageToFrame(childNode, window, webEngine, frame);
952 childNode = childNode.getNextSibling();
953 }
954 }
955 }
956
957 private static boolean elementVisible(float x, float y, float width, float height, JSObject style) {
958 if (width <= 0 || height <= 0 || x + width <= 0 || y + height <= 0 || ((String) style.call("getPropertyValue", new Object[] { "visibility" })).equals("hidden")
959 || ((String) style.call("getPropertyValue", new Object[] { "display" })).equals("none")) {
960 return false;
961 } else {
962 return true;
963 }
964 }
965
966 /**
967 * @param imgSrc
968 * URL of the image to add
969 * @param linkUrl
970 * Absolute URL that the image should link to when clicked
971 * @param frame
972 * Frame to add the image to
973 * @param x
974 * X-coordinate at which the image should be placed on the frame
975 * @param y
976 * Y-coordinate at which the image should be placed on the frame
977 * @param width
978 * Width of the image once added to the frame. Negative 1 (-1) will cause the actual width of the image file to be used
979 *
980 * @param cropStartX
981 * X-coordinate at which to start crop, or null for no crop
982 * @param cropStartY
983 * Y-coordinate at which to start crop, or null for no crop
984 * @param cropEndX
985 * X-coordinate at which to end the crop, or null for no crop
986 * @param cropEndY
987 * Y-coordinate at which to end the crop, or null for no crop
988 *
989 * @param repeat
990 * String determining how the image should be tiled/repeated. Valid strings are: <i>no-repeat</i>, <i>repeat-x</i>, or
991 * <i>repeat-y</i>. All other values (including null) will cause the image to repeat in both directions
992 *
993 * @param originXPercent
994 * Percentage into the image to use as the x coordinate of the image's origin point
995 * @param originYPercent
996 * Percentage into the image to use as the y coordinate of the image's origin point
997 *
998 * @throws MalformedURLException
999 * @throws IOException
1000 */
1001 public static Picture getImageFromUrl(String imgSrc, String linkUrl, final Frame frame, float x, float y, int width,
1002 Integer cropStartX, Integer cropStartY, Integer cropEndX, Integer cropEndY, String repeat, float originXPercent, float originYPercent)
1003 throws IOException {
1004
1005 URL imgUrl = new URL(imgSrc);
1006
1007 HttpURLConnection connection = (HttpURLConnection) (imgUrl.openConnection());
1008
1009 // Spoofing a widely accepted User Agent, since some sites refuse to serve non-webbrowser clients
1010 connection.setRequestProperty("User-Agent", "Mozilla/5.0");
1011
1012 BufferedImage img = ImageIO.read(connection.getInputStream());
1013
1014 int hashcode = Arrays.hashCode(img.getData().getPixels(0, 0, img.getWidth(), img.getHeight(), (int[]) null));
1015 File out = new File(FrameIO.IMAGES_PATH + Integer.toHexString(hashcode) + ".png");
1016 out.mkdirs();
1017 ImageIO.write(img, "png", out);
1018
1019 if (repeat == null && cropEndX == null && cropStartX == null && cropEndY == null && cropStartY == null) {
1020 repeat = "no-repeat";
1021 }
1022
1023 if (cropEndX == null || cropStartX == null || cropEndY == null || cropStartY == null) {
1024 cropStartX = 0;
1025 cropStartY = 0;
1026 cropEndX = img.getWidth();
1027 cropEndY = img.getHeight();
1028 } else if (cropStartX < 0) {
1029 cropEndX = cropEndX - cropStartX;
1030 x = x + Math.abs(cropStartX);
1031 cropStartX = 0;
1032 }
1033
1034 if (cropStartY < 0) {
1035 cropEndY = cropEndY - cropStartY;
1036 y = y + Math.abs(cropStartY);
1037 cropStartY = 0;
1038 }
1039
1040 if (width < 0) {
1041 width = img.getWidth();
1042 }
1043
1044 if (repeat != null) {
1045 if (repeat.equals("no-repeat")) {
1046 int tmpCropEndY = (int) (cropStartY + ((float) width / img.getWidth()) * img.getHeight());
1047 int tmpCropEndX = cropStartX + width;
1048
1049 cropEndX = (cropEndX < tmpCropEndX) ? cropEndX : tmpCropEndX;
1050 cropEndY = (cropEndY < tmpCropEndY) ? cropEndY : tmpCropEndY;
1051 } else if (repeat.equals("repeat-x")) {
1052 int tmpCropEndY = (int) (cropStartY + ((float) width / img.getWidth()) * img.getHeight());
1053 cropEndY = (cropEndY < tmpCropEndY) ? cropEndY : tmpCropEndY;
1054 } else if (repeat.equals("repeat-y")) {
1055 int tmpCropEndX = cropStartX + width;
1056 cropEndX = (cropEndX < tmpCropEndX) ? cropEndX : tmpCropEndX;
1057 }
1058 }
1059
1060 if (originXPercent > 0) {
1061 int actualWidth = cropEndX - cropStartX;
1062
1063 int originXPixels = Math.round(originXPercent * actualWidth);
1064
1065 x = x - originXPixels;
1066
1067 cropStartX = (int) (cropStartX + (width - actualWidth) * originXPercent);
1068 cropEndX = (int) (cropEndX + (width - actualWidth) * originXPercent);
1069 }
1070
1071 if (originYPercent > 0) {
1072 int height = (int) ((img.getHeight() / (float) img.getWidth()) * width);
1073 int actualHeight = (cropEndY - cropStartY);
1074 int originYPixels = Math.round(originYPercent * actualHeight);
1075
1076 y = y - originYPixels;
1077
1078 cropStartY = (int) (cropStartY + (height - actualHeight) * originYPercent);
1079 cropEndY = (int) (cropEndY + (height - actualHeight) * originYPercent);
1080 }
1081
1082 Text text = new Text("@i: " + out.getName() + " " + width);
1083 text.setPosition(x, y);
1084
1085 Picture pic = ItemUtils.CreatePicture(text, frame);
1086
1087 float invScale = 1 / pic.getScale();
1088
1089 pic.setCrop((int)(cropStartX * invScale), (int)(cropStartY * invScale), (int)(cropEndX * invScale), (int)(cropEndY * invScale));
1090
1091 if (linkUrl != null && !linkUrl.equals("undefined")) {
1092 pic.setAction("goto " + linkUrl);
1093 pic.setActionMark(false);
1094 }
1095
1096 return pic;
1097 }
1098
1099 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,
1100 float originXPercent, float originYPercent)
1101 throws IOException {
1102 Picture pic = getImageFromUrl(imgSrc, linkUrl, frame, x, y, width, cropStartX, cropStartY, cropEndX, cropEndY, repeat, originXPercent, originYPercent);
1103 frame.addItem(pic);
1104 pic.anchor();
1105 pic.getSource().anchor();
1106 }
1107
1108 public static Picture getBackgroundImageFromNode(Node node, JSObject style, final Frame frame, String linkUrl, float x, float y, float width, float height) throws IOException {
1109
1110
1111 String bgImage = (String) style.call("getPropertyValue", new Object[] { "background-image" });
1112 bgImage = bgImage.substring(4, bgImage.length() - 1);
1113
1114 String bgSize = ((String) style.call("getPropertyValue", new Object[] { "background-size" })).toLowerCase();
1115 String bgRepeat = ((String) style.call("getPropertyValue", new Object[] { "background-repeat" })).toLowerCase();
1116
1117 // Returns "[x]px [y]px", "[x]% [y]%", "[x]px [y]%" or "[x]% [y]px"
1118 String bgPosition = ((String) style.call("getPropertyValue", new Object[] { "background-position" })).toLowerCase();
1119
1120 String[] bgOffsetCoords = bgPosition.split(" ");
1121
1122 int bgOffsetX = 0, bgOffsetY = 0;
1123
1124 float originXPercent = 0, originYPercent = 0;
1125
1126 int cropStartX, cropStartY, cropEndX, cropEndY;
1127
1128 // Converting the x and y offset values to integers (and from % to px if needed)
1129 if (bgOffsetCoords[0].endsWith("%")) {
1130 bgOffsetX = (int) ((Integer.valueOf(bgOffsetCoords[0].substring(0, bgOffsetCoords[0].length() - 1)) / 100.0) * width);
1131 originXPercent = (Integer.valueOf(bgOffsetCoords[0].substring(0, bgOffsetCoords[0].length() - 1))) / 100f;
1132 } else if (bgOffsetCoords[0].endsWith("px")) {
1133 bgOffsetX = (int) (Integer.valueOf(bgOffsetCoords[0].substring(0, bgOffsetCoords[0].length() - 2)));
1134 }
1135
1136 if (bgOffsetCoords[1].endsWith("%")) {
1137 bgOffsetY = (int) ((Integer.valueOf(bgOffsetCoords[1].substring(0, bgOffsetCoords[1].length() - 1)) / 100.0) * height);
1138 originYPercent = (Integer.valueOf(bgOffsetCoords[1].substring(0, bgOffsetCoords[1].length() - 1))) / 100f;
1139 } else if (bgOffsetCoords[1].endsWith("px")) {
1140 bgOffsetY = (int) (Integer.valueOf(bgOffsetCoords[1].substring(0, bgOffsetCoords[1].length() - 2)));
1141 }
1142
1143 // Converting from an offset to crop coords
1144 cropStartX = -1 * bgOffsetX;
1145 cropEndX = (int) (cropStartX + width);
1146
1147 cropStartY = -1 * bgOffsetY;
1148 cropEndY = (int) (cropStartY + height);
1149
1150 int bgWidth = -1;
1151
1152 if (bgSize.equals("cover")) {
1153 bgWidth = (int) width;
1154 } else if (bgSize.equals("contain")) {
1155 // TODO: actually compute the appropriate width
1156 bgWidth = (int) width;
1157 } else if (bgSize.equals("auto")) {
1158 bgWidth = -1;
1159 } else {
1160 bgSize = bgSize.split(" ")[0];
1161
1162 if (bgSize.endsWith("%")) {
1163 bgWidth = (int) ((Integer.parseInt(bgSize.replaceAll("\\D", "")) / 100.0) * width);
1164 } else if (bgSize.endsWith("px")) {
1165 bgWidth = Integer.parseInt(bgSize.replaceAll("\\D", ""));
1166 }
1167 }
1168
1169 return getImageFromUrl(bgImage, linkUrl, frame, x, y, bgWidth, cropStartX, cropStartY, cropEndX, cropEndY, bgRepeat, originXPercent, originYPercent);
1170 }
1171
1172 private static void addBackgroundImageFromNode(Node node, JSObject style, final Frame frame, String linkUrl, float x, float y, float width, float height) throws IOException {
1173 Picture pic = getBackgroundImageFromNode(node, style, frame, linkUrl, x, y, width, height);
1174 frame.addItem(pic);
1175 pic.anchor();
1176 pic.getSource().anchor();
1177 }
1178
1179 /**
1180 * @param rootElement
1181 * Element that will be converted (including all sub-elements)
1182 * @param backgroundColor
1183 * 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)"
1184 * @param window
1185 * 'window' from Javascript
1186 * @param webEngine
1187 * Web engine that the page is loaded in
1188 * @param frame
1189 * Expeditee frame to add the converted page to
1190 * @throws IllegalArgumentException
1191 * @throws IllegalAccessException
1192 */
1193 private static void addTextToFrame(int visibleWidth, int visibleHeight, JSObject window, WebEngine webEngine, Frame frame) throws InvocationTargetException,
1194 IllegalAccessException, IllegalArgumentException {
1195
1196 webEngine.executeScript("var walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null, false);");
1197
1198 Node currentNode;
1199
1200 while ((currentNode = (Node) webEngine.executeScript("walker.nextNode()")) != null) {
1201 JSObject style;
1202 JSObject bounds;
1203
1204 // CSS style for the element
1205 style = (JSObject) window.call("getComputedStyle", new Object[] { currentNode.getParentNode() });
1206
1207 // Getting a rectangle that represents the area and position of the element
1208 bounds = (JSObject) ((JSObject) currentNode.getParentNode()).call("getBoundingClientRect", new Object[] {});
1209
1210 // 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
1211 // be to walking through the element's parents until the document root is reached (or a recursive function)
1212 float x = Float.valueOf(bounds.getMember("left").toString());
1213 float y = Float.valueOf(bounds.getMember("top").toString());
1214
1215 float width = Float.valueOf(bounds.getMember("width").toString());
1216 float height = Float.valueOf(bounds.getMember("height").toString());
1217
1218 // Checking if the element is actually visible on the page
1219 if (width > 0 && height > 0 && x + width > 0 && y + height > 0 && x <= visibleWidth && y <= visibleHeight
1220 && !(((String) style.call("getPropertyValue", new Object[] { "display" })).equals("none"))
1221 && !(((String) style.call("getPropertyValue", new Object[] { "visibility" })).equals("hidden"))) {
1222
1223 String fontSize = ((String) style.call("getPropertyValue", new Object[] { "font-size" }));
1224
1225 // Trimming off the units (always px) from the font size
1226 fontSize = fontSize.substring(0, fontSize.length() - 2);
1227
1228 // Always returns in format "rgb(x,x,x)" or "rgba(x,x,x,x)"
1229 String color = (String) style.call("getPropertyValue", new Object[] { "color" });
1230
1231 // Always returns in format "rgb(x,x,x)" or "rgba(x,x,x,x)"
1232 String bgColorString = (String) style.call("getPropertyValue", new Object[] { "background-color" });
1233
1234 String align = (String) style.call("getPropertyValue", new Object[] { "text-align" });
1235
1236 // Returns comma-separated list of typefaces
1237 String typeface = (String) style.call("getPropertyValue", new Object[] { "font-family" });
1238
1239 String[] typefaces = typeface.split(", |,");
1240
1241 String weight = (String) style.call("getPropertyValue", new Object[] { "font-weight" });
1242
1243 String fontStyle = (String) style.call("getPropertyValue", new Object[] { "font-style" });
1244
1245 // Returns "normal" or a value in pixels (e.g. "10px")
1246 String letterSpacing = (String) style.call("getPropertyValue", new Object[] { "letter-spacing" });
1247
1248 // Returns a value in pixels (e.g. "10px")
1249 String lineHeight = (String) style.call("getPropertyValue", new Object[] { "line-height" });
1250
1251 String textTransform = (String) style.call("getPropertyValue", new Object[] { "text-transform" });
1252
1253 String linkUrl = (String) ((JSObject) currentNode.getParentNode()).getMember("href");
1254
1255 Boolean fontFound = false;
1256 Font font = new Font(null);
1257
1258 // Looping through all font-families listed in the element's CSS until one that is installed is
1259 // found, or the end of the list is reached, in which case the default font is used
1260 for (int j = 0; j < typefaces.length && !fontFound; j++) {
1261 if (typefaces[j].toLowerCase().equals("sans-serif")) {
1262 typefaces[j] = "SansSerif";
1263 } else if ((typefaces[j].toLowerCase().equals("arial"))) {
1264 // Have to use Arial Unicode, otherwise unicode characters display incorrectly
1265 // It seems that not all systems have this font (including some Windows machines),
1266 // but as long as the website has a general font type specified (e.g. "font-family: Arial, Sans-Serif"),
1267 // there should be no noticeable difference.
1268 typefaces[j] = "Arial Unicode MS";
1269 } else if ((typefaces[j].toLowerCase().equals("monospace"))) {
1270 typefaces[j] = "monospaced";
1271 }
1272
1273 // Regex will remove any inverted commas surrounding multi-word typeface names
1274 font = new Font(typefaces[j].replaceAll("^'|'$", ""), Font.PLAIN, 12);
1275
1276 // If the font isn't found, Java just uses Font.DIALOG, so this check checks whether the font was found
1277 if (!(font.getFamily().toLowerCase().equals(Font.DIALOG.toLowerCase()))) {
1278 fontFound = true;
1279 }
1280 }
1281
1282 if (font.getFamily().toLowerCase().equals(Font.DIALOG.toLowerCase())) {
1283 font = new Font("Times New Roman", Font.PLAIN, 12);
1284 }
1285
1286 String fontStyleComplete = "";
1287
1288 int weightInt = 0;
1289
1290 try {
1291 weightInt = Integer.parseInt(weight);
1292 } catch (NumberFormatException nfe) {
1293 // Use default value as set above
1294 }
1295
1296 // checking if font is bold - i.e. 'bold', 'bolder' or weight over 500
1297 if (weight.toLowerCase().startsWith("bold") || weightInt > 500) {
1298 fontStyleComplete = fontStyleComplete.concat("bold");
1299 }
1300
1301 if (fontStyle.toLowerCase().equals("italic") || fontStyle.toLowerCase().equals("oblique")) {
1302 fontStyleComplete = fontStyleComplete.concat("italic");
1303 }
1304
1305 float fontSizeFloat = 12;
1306
1307 try {
1308 fontSizeFloat = Float.valueOf(fontSize);
1309 } catch (NumberFormatException nfe) {
1310 // Use default value as set above
1311 }
1312
1313 float letterSpacingFloat = -0.008f;
1314
1315 try {
1316 letterSpacingFloat = (Integer.parseInt(letterSpacing.substring(0, letterSpacing.length() - 2)) / (fontSizeFloat));
1317 } catch (NumberFormatException nfe) {
1318 // Use default value as set above
1319 }
1320
1321 float lineHeightInt = -1;
1322
1323 try {
1324 lineHeightInt = (Float.parseFloat(lineHeight.substring(0, lineHeight.length() - 2)));
1325 } catch (NumberFormatException nfe) {
1326 // Use default value as set above
1327 }
1328
1329 Text t;
1330
1331 String textContent = currentNode.getTextContent().replaceAll("[^\\S\\n]+", " ");
1332 textContent = textContent.replaceAll("^(\\s)(\\n|\\r)", "");
1333
1334 if (textTransform.equals("uppercase")) {
1335 textContent = textContent.toUpperCase();
1336 } else if (textTransform.equals("lowercase")) {
1337 textContent = textContent.toUpperCase();
1338 }
1339
1340 // Adding the text to the frame. Expeditee text seems to be positioned relative to the baseline of the first line, so
1341 // the font size has to be added to the y-position
1342 t = frame.addText(Math.round(x), Math.round(y + fontSizeFloat), textContent, null);
1343
1344 t.setColor(rgbStringToColor(color));
1345 t.setBackgroundColor(rgbStringToColor(bgColorString));
1346 t.setFont(font);
1347 t.setSize(fontSizeFloat);
1348 t.setFontStyle(fontStyleComplete);
1349 t.setLetterSpacing(letterSpacingFloat);
1350
1351 // Removing any spacing between lines allowing t.getLineHeight() to be used to get the actual height
1352 // of just the characters (i.e. distance from ascenders to descenders)
1353 t.setSpacing(0);
1354
1355 t.setSpacing(lineHeightInt - t.getLineHeight());
1356
1357 if (align.equals("left")) {
1358 t.setJustification(Justification.left);
1359 } else if (align.equals("right")) {
1360 t.setJustification(Justification.right);
1361 } else if (align.equals("center")) {
1362 t.setJustification(Justification.center);
1363 } else if (align.equals("justify")) {
1364 t.setJustification(Justification.full);
1365 }
1366
1367 // Font size is added to the item width to give a little breathing room
1368 t.setWidth(Math.round(width + (t.getSize())));
1369
1370 if (!linkUrl.equals("undefined")) {
1371 t.setAction("createFrameWithBrowser " + linkUrl);
1372 t.setActionMark(false);
1373 }
1374 }
1375 }
1376 }
1377
1378 /**
1379 * Used by the web parser to add Next, Previous, etc. buttons to the converted pages
1380 *
1381 * @param text
1382 * text to display on the button
1383 * @param link
1384 * Frame that the button will link to
1385 * @param action
1386 * Action to run when button is clicked
1387 * @param width
1388 * Width of the button
1389 * @param toAddTo
1390 * Frame to add the button to
1391 * @param anchorTop
1392 * @param anchorRight
1393 * @param anchorBottom
1394 * @param anchorLeft
1395 */
1396 private static void addButton(String text, String link, String action, int width, Frame toAddTo, Float anchorTop, Float anchorRight, Float anchorBottom, Float anchorLeft) {
1397 // Button to go to the next frame/page
1398 Text button = new Text(text);
1399
1400 button.setLink(link);
1401 button.addAction(action);
1402 button.setBorderColor(new Color(0.7f, 0.7f, 0.7f));
1403 button.setBackgroundColor(new Color(0.9f, 0.9f, 0.9f));
1404 button.setThickness(1);
1405 button.setLinkMark(false);
1406 button.setActionMark(false);
1407 button.setFamily("Roboto Condensed Light");
1408 button.setJustification(Justification.center);
1409 button.setWidth(width);
1410
1411 if (anchorTop != null) {
1412 button.setAnchorTop(anchorTop);
1413 }
1414
1415 if (anchorRight != null) {
1416 button.setAnchorRight(anchorRight);
1417 }
1418
1419 if (anchorBottom != null) {
1420 button.setAnchorBottom(anchorBottom);
1421 }
1422
1423 if (anchorLeft != null) {
1424 button.setAnchorLeft(anchorLeft);
1425 }
1426
1427 button.setID(toAddTo.getNextItemID());
1428 toAddTo.addItem(button);
1429
1430 }
1431
1432
1433 private static class MutableBool {
1434 private boolean value;
1435
1436 public MutableBool(boolean value) {
1437 this.value = value;
1438 }
1439
1440 public boolean getValue() {
1441 return value;
1442 }
1443
1444 public void setValue(boolean value) {
1445 this.value = value;
1446 }
1447 }
1448
1449 private static class MutableInt {
1450 private int value;
1451
1452 public MutableInt(int value) {
1453 this.value = value;
1454 }
1455
1456 public int getValue() {
1457 return value;
1458 }
1459
1460 public void setValue(int value) {
1461 this.value = value;
1462 }
1463 }
1464}
Note: See TracBrowser for help on using the repository browser.