N'ayant pas trouvé d'application light et simple me permettant de lire mes livres au format epub sur n'importe quel OS je me suis amusé à progammer une application java permettant de lire des fichiers epub ; pour cela j'ai un peu "joué" avec le composant JavaFX "WebView" . J'ai décidé de faire ce petit POST (le dernier date de longtemps) pour partager les problèmes que j'ai rencontré et les solutions que j'ai fini pas trouver en cherchant à gauche et à droite. En effet, si tout n'est pas pas trivial je n'ai pour l'instant pas été confronté à un problème insoluble.
Le b.a-ba : charger une page contenue dans une String
WebEngine engine = ebookView.getEngine(); Optional<String> page = ... // load the page in memory engine.loadContent(page.get()); // load the page in the webview
Intercepter le click sur les liens
Enregistrer un EventListener sur les noeud DOM de notre choix
Les fichiers epub contiennent une table des matières permettant d'aller à une page particulière du livre, sous la forme d'une liste de liens. Ceux-ci sont par exemple sous la forme suivante :
<a href="pages/48297a6e74f19d19617e8a796ba1a73d.xhtml">page suivante</a>
Le problème est que le moteur du WebEngine ne sait pas résoudre ces liens ; c'est normal puisque ceux-ci sont des liens relatifs à la page courante qui est elle même contenue dans le fichier epub. Pour résoudre cette problématique j'ai cherché comment intercepter le click sur ce lien pour pouvoir ensuite aller chercher l'HTML attendu et le charger dans le webview. La solution m'a été inspirée de la javadoc du composant WebEngine et consiste à la fin du chargement de la page (State.SUCCEEDED) pour chaque balise "a" trouvée dans l'html à rajouter un listener (code java) qui sera automatiquement appelé par le moteur javascript du WebEngine. Le rajout de ce listener se fait à la fin du chargement de la page via le code suivant :
ebookView.getEngine().getLoadWorker().stateProperty().addListener(new ChangeListener<Worker.State>() {
@Override
public void changed(ObservableValue ov, Worker.State oldState, Worker.State newState) {
if (newState == Worker.State.SUCCEEDED) {
EventListener listener = new EventListener() {
@Override
public void handleEvent(Event ev) {
if ("click".equals(ev.getType())) {
String href = ((Element)ev.getCurrentTarget()).getAttribute("href");
if (href==null) return;
Platform.runLater(()->currentEBook.ifPresent(e->ebookView.getEngine().loadContent(e.loadPage(href).orElse(""))));
}
}
};
Document doc = ebookView.getEngine().getDocument();
NodeList nodeList = doc.getElementsByTagName("a");
for (int i = 0; i < nodeList.getLength(); i++) {
((EventTarget) nodeList.item(i)).addEventListener("click", listener, true);
}
}
}
});
Ce code est intéressant car il montre que l'on peut manipuler l'arbre DOM de la page HTML courante : java offre une implementation de la spécification DOM.
Analysons plus en détail la ligne suivante :
((EvenChartTarget) nodeList.item(i)).addEventListener("click", listener, true);
En gros chaque noeud correspondant à une balise "a" est castée en EventTarget ce qui permet de lui associer l'EventListener "listener" créé quelques lignes au dessus. En théorie le cast doit être testé (voir spec DOM), mais dans notre cas nous savons que les balises "a" conviennent.
Problématique des balises filles de la balise "a"
Le troisième paramètre de la méthode addEventListener doit valoir "true". En effet, avant de passer ce paramètre à true j'avais certains liens pour lesquels la valeur de l'attribut href était toujours null au niveau de la ligne suivante :
String href = ((Element)ev.getCurrentTarget()).getAttribute("href");
Après avoir creusé, ce problème se posait pour des liens tels que celui là :
<a href="e9782919755387_fm01.html" class="toc_entry_frontMatter"><span class="b">Prologue</span></a>
Bien que l'EventListener soit enregistré sur la balise "a", par défaut c'est la balise fille "span" qui était retournée par la méthode getCurrentTarget et évidemment cette balise n'a pas d'attribut href - d'où la valeur null récupérée. Si l'on passe par contre le troisième paramètre à true on a le fonctionnement attendu : le listener est appelé avec la balise "a".
En fait il ne s'agit pas d'un bug, mais la spec DOM explique que l'événement est transmis à la balise la plus profonde ; sauf en passant ce paramètre à true. On aurait aussi pu aussi faire remonter (event bubbling) l'événement depuis la balise "span" vers la balise "a" : tout çà est décrit sur la page de spécification suivante pour ceux qui voudraient creuser : vous pouvez aller sur la specification du w3 ici.
Afficher des images "locales"
La solution est assez proche de celle utilisée pour les liens, elle consiste lorsque le document est chargé (Worker.State.SUCCEEDED) à remplacer la valeur de l'attribut src de la balise "img" par une URL que le moteur pourra résoudre. En l'occurrence une URL sous la forme suivante :
jar:file://<path to epub file>!<img path in the epub file>
Le code complet :
ebookView.getEngine().getLoadWorker().stateProperty().addListener(new ChangeListener() { @Override public void changed(ObservableValue ov, Worker.State oldState, Worker.State newState) { if (newState == Worker.State.SUCCEEDED) { Document doc = ebookView.getEngine().getDocument(); NodeList nodeList = doc.getElementsByTagName("img"); for (int i = 0; i < nodeList.getLength(); i++) { Element img = (Element) nodeList.item(i); String href = img.getAttribute("src"); // replace local url by global url in epub file currentEBook.map(e -> e.convertRessourceLocalPathToGlobalURL(href).orElse(null)).ifPresent(s->img.setAttribute("src",s)); } } } });
Autres Liens intéressants
- Une page qui m'a permis de comprendre la structure des fichiers epub.
- Javadoc JFX complète
- Le résultat de mes petites expérimentations avec le WebView et les fichiers epub peut être chargé ici
Comment l'utiliser :
- View ebooks :
java -jar <path to jar> --gui - To display informations about an ebook
java -jar <path to jar> --info --target "<path to epub file>" - To add a category (Fantastic in this example) to ebook file
java -jar <path to jar> --addSubject "Fantastic" --target "<path to epub file>" - To remove a category (Littérature in this example) from ebook file
java -jar <path to jar> --removeSubject "Littérature" --target "<path to epub file>"
- View ebooks :
Aucun commentaire:
Enregistrer un commentaire