0. Índice de contenidos.
- 1. Introducción
- 2. Entorno
- 3. Extrayendo la metainformación de un libro
- 4. Leyendo un Epub
- 5. Conclusiones
- 6. Información sobre el autor
1. Introducción
Hoy vamos a ver cómo podemos leer Epub (Electronic Publication) desde Java. Existen algunas librerías para el parseo, la lectura y la generación de Epub, y tras probar algunas, encontramos una que funciona realmente bien y de forma muy sencilla. Se llama EpubLib.
EpubLib es un API de Java desarrollado por Paul Siegmann y que os podéis descargar de su página principalhttp://www.siegmann.nl/. Está licenciado bajo LGPL. Si queréis saber más sobre la licencia podéis consultar los términos en http://www.gnu.org/licenses/lgpl-java.html
2. Entorno
- Hardware: Portátil MacBook Pro 15′ (2.0 GHz Intel i7, 8GB DDR3 SDRAM, 500GB HDD).
- AMD Radeon HD 6490M 256 MB
- Sistema Operativo: Mac OS X Snow Leopard 10.6.7
- Software: Eclipse Helios, EpubLib 3.0-SNAPSHOT
3. Extrayendo la metainformación de un libro
Para este tutorial vamos a utilizar el epub «Treasure Island» o en español, «La isla del Tesoro». Los epub no son más que un fichero comprimido con extensión .epub, y si los descomprimimos dentro tienen varios ficheros en formato xhtml. De entre ellos, uno de los más importantes es aquel con extensión .opf. En este fichero se especifica la metainformación del libro: Autor, editorial, tema del libro, descripción, etc…
A continuación vamos a ver el contenido del fichero content.opf de nuestro epub Treasure Island. Empieza de la siguiente forma.
1 2 3 4 5 6 7 8 9 10 11 |
<?xml version="1.0" ?> <package version="2.0" xmlns="http://www.idpf.org/2007/opf" unique-identifier="BookId"> <metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf"> <dc:title>Treasure Island</dc:title> <dc:creator>Robert Louis Stevenson</dc:creator> <dc:date>2009-02-14</dc:date> <dc:subject>Youth</dc:subject> <dc:language>en</dc:language> <dc:publisher>Web Books Publishing</dc:publisher> <dc:identifier id="BookId">web-books-309</dc:identifier> </metadata> |
Para extraer esta información con EpubLib tan solo tendríamos que hacer lo siguiente:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
package com.autentia.EpubLibTest; import static org.junit.Assert.*; import java.net.URL; import nl.siegmann.epublib.domain.Book; import nl.siegmann.epublib.epub.EpubReader; import org.junit.BeforeClass; import org.junit.Test; import org.springframework.core.io.ClassPathResource; public class ReaderTest { private static URL bookURL; private static EpubReader epubReader; private static Book book; @BeforeClass public static void setBook() throws Exception { bookURL = new ClassPathResource("Treasure_Island.epub").getURL(); epubReader = new EpubReader(); book = epubReader.readEpub(bookURL.openStream()); } @Test public void metadataIsCorrect() { assertEquals("Robert Louis", book.getMetadata().getAuthors().get(0).getFirstname()); assertEquals("Stevenson", book.getMetadata().getAuthors().get(0).getLastname()); assertEquals("application/epub+zip", book.getMetadata().getFormat()); assertEquals("en", book.getMetadata().getLanguage()); assertEquals("Youth", book.getMetadata().getSubjects().get(0)); assertEquals("Treasure Island", book.getMetadata().getTitles().get(0)); assertEquals("web-books-309", book.getMetadata().getIdentifiers().get(0).getValue()); assertEquals("2009-02-14", book.getMetadata().getDates().get(0).getValue()); } } |
Como veis, tan solo me he creado un método setBook (línea 23) en un @BeforeClass para cargar el libro una sola vez. De esta forma estará disponible para todos los métodos de test de esta clase. En este primer test comprobamos que la metainformación se corresponde con lo que tiene el archivo content.opf.
Si os fijáis en la línea 25 estamos usando ClassPathResource, que es una clase de Spring. Si queréis cargar el fichero .epubde esta forma solo tenéis que añadir a vuestro POM la siguiente dependencia.
1 2 3 4 5 |
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>3.0.5.RELEASE</version> </dependency> |
4. Leyendo un Epub
Vamos a meternos dentro del archivo content.opf de nuevo. Esta vez nos vamos a fijar más abajo. Concretamente en los apartados <manifest> y <spine toc=»ncx»>. El contenido es el siguiente:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
<manifest> <item id="ncx" href="toc.ncx" media-type="application/x-dtbncx+xml" /> <item id="W000Title" href="000Title.html" media-type="application/xhtml+xml" /> <item id="W01ZB309" href="01ZB309.html" media-type="application/xhtml+xml" /> <item id="W021B309" href="021B309.html" media-type="application/xhtml+xml" /> <item id="W03MB309" href="03MB309.html" media-type="application/xhtml+xml" /> <item id="W04MB309" href="04MB309.html" media-type="application/xhtml+xml" /> <item id="W05MB309" href="05MB309.html" media-type="application/xhtml+xml" /> <item id="W06MB309" href="06MB309.html" media-type="application/xhtml+xml" /> <item id="W07MB309" href="07MB309.html" media-type="application/xhtml+xml" /> <item id="W08ZB309" href="08ZB309.html" media-type="application/xhtml+xml" /> <item id="W091B309" href="091B309.html" media-type="application/xhtml+xml" /> <item id="W10MB309" href="10MB309.html" media-type="application/xhtml+xml" /> <item id="W11MB309" href="11MB309.html" media-type="application/xhtml+xml" /> <item id="W12MB309" href="12MB309.html" media-type="application/xhtml+xml" /> <item id="W13MB309" href="13MB309.html" media-type="application/xhtml+xml" /> <item id="W14MB309" href="14MB309.html" media-type="application/xhtml+xml" /> <item id="W15ZB309" href="15ZB309.html" media-type="application/xhtml+xml" /> <item id="W161B309" href="161B309.html" media-type="application/xhtml+xml" /> <item id="W17MB309" href="17MB309.html" media-type="application/xhtml+xml" /> <item id="W18MB309" href="18MB309.html" media-type="application/xhtml+xml" /> <item id="W19ZB309" href="19ZB309.html" media-type="application/xhtml+xml" /> <item id="W201B309" href="201B309.html" media-type="application/xhtml+xml" /> <item id="W21MB309" href="21MB309.html" media-type="application/xhtml+xml" /> <item id="W22MB309" href="22MB309.html" media-type="application/xhtml+xml" /> <item id="W23MB309" href="23MB309.html" media-type="application/xhtml+xml" /> <item id="W24MB309" href="24MB309.html" media-type="application/xhtml+xml" /> <item id="W25MB309" href="25MB309.html" media-type="application/xhtml+xml" /> <item id="W26ZB309" href="26ZB309.html" media-type="application/xhtml+xml" /> <item id="W271B309" href="271B309.html" media-type="application/xhtml+xml" /> <item id="W28MB309" href="28MB309.html" media-type="application/xhtml+xml" /> <item id="W29MB309" href="29MB309.html" media-type="application/xhtml+xml" /> <item id="W30MB309" href="30MB309.html" media-type="application/xhtml+xml" /> <item id="W31MB309" href="31MB309.html" media-type="application/xhtml+xml" /> <item id="W32MB309" href="32MB309.html" media-type="application/xhtml+xml" /> <item id="W33ZB309" href="33ZB309.html" media-type="application/xhtml+xml" /> <item id="W341B309" href="341B309.html" media-type="application/xhtml+xml" /> <item id="W35MB309" href="35MB309.html" media-type="application/xhtml+xml" /> <item id="W36MB309" href="36MB309.html" media-type="application/xhtml+xml" /> <item id="W37MB309" href="37MB309.html" media-type="application/xhtml+xml" /> <item id="W38MB309" href="38MB309.html" media-type="application/xhtml+xml" /> <item id="W39MB309" href="39MB309.html" media-type="application/xhtml+xml" /> <item id="W40MB309" href="40MB309.html" media-type="application/xhtml+xml" /> <item id="W41ZB309" href="41ZB309.html" media-type="application/xhtml+xml" /> <item id="WTOC" href="TOC.html" media-type="application/xhtml+xml" /> <item id="style" href="style.css" media-type="text/css" /> <item id="cover" href="cover.jpg" media-type="image/jpeg" /> </manifest> <spine toc="ncx"> <itemref idref="W000Title" /> <itemref idref="W01ZB309" /> <itemref idref="W021B309" /> <itemref idref="W03MB309" /> <itemref idref="W04MB309" /> <itemref idref="W05MB309" /> <itemref idref="W06MB309" /> <itemref idref="W07MB309" /> <itemref idref="W08ZB309" /> <itemref idref="W091B309" /> <itemref idref="W10MB309" /> <itemref idref="W11MB309" /> <itemref idref="W12MB309" /> <itemref idref="W13MB309" /> <itemref idref="W14MB309" /> <itemref idref="W15ZB309" /> <itemref idref="W161B309" /> <itemref idref="W17MB309" /> <itemref idref="W18MB309" /> <itemref idref="W19ZB309" /> <itemref idref="W201B309" /> <itemref idref="W21MB309" /> <itemref idref="W22MB309" /> <itemref idref="W23MB309" /> <itemref idref="W24MB309" /> <itemref idref="W25MB309" /> <itemref idref="W26ZB309" /> <itemref idref="W271B309" /> <itemref idref="W28MB309" /> <itemref idref="W29MB309" /> <itemref idref="W30MB309" /> <itemref idref="W31MB309" /> <itemref idref="W32MB309" /> <itemref idref="W33ZB309" /> <itemref idref="W341B309" /> <itemref idref="W35MB309" /> <itemref idref="W36MB309" /> <itemref idref="W37MB309" /> <itemref idref="W38MB309" /> <itemref idref="W39MB309" /> <itemref idref="W40MB309" /> <itemref idref="W41ZB309" /> </spine> |
Si os fijáis en la línea 51, tenemos un item con id = W01ZB309. Y en la línea 4, tenemos el archivo al que se está referenciando con ese id, que es 01ZB309.html. Este archivo se corresponde con la posición 1 de <spine toc=»ncx»>, que no es ni más ni menos que «toc» table of contents o tabla de contenidos. Este archivo es por lo tanto el contenido nº 1 de la tabla de contenidos. El 0 sería 000Title.html ya que es lo primero que se mostrará del libro, y los sucesivos se sacan de igual forma. En la línea 45 podemos ver otro archivo que define la tabla de contenidos. Es TOC.html y además de los archivos que representan cada contenido, viene el título que se debería mostrar asociado a dicho archivo.
4.1 Lectura Síncrona
Vamos a ver ahora, como acceder a esta información con EpubLib, con un test muy sencillo, que podemos situar debajo del anterior. Lo que vamos a leer es el contenido precisamente de nuestro recurso 1 de la tabla de contenidos, es decir, el archivo01ZB309.html, que os pongo a continuación:
01ZB309.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
<?xml version="1.0" encoding="utf-8" ?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="application/xhtml+xml; charset=utf-8" /> <meta name="generator" content="Web Books Publishing" /> <link rel="stylesheet" type="text/css" href="style.css" /> <title>To S.L.O. and the Hesitating Purchaser</title> </head> <body> <div class='header'> <h3>To S.L.O. and the Hesitating Purchaser</h3> </div> <blockquote><p> To S.L.O., an American gentleman in accordance with whose classic taste the following narrative has been designed, it is now, in return for numerous delightful hours, and with the kindest wishes, dedicated by his affectionate friend, the author. </p></blockquote> <p><br /></p> <pre> TO THE HESITATING PURCHASER If sailor tales to sailor tunes, Storm and adventure, heat and cold, If schooners, islands, and maroons, And buccaneers, and buried gold, And all the old romance, retold Exactly in the ancient way, Can please, as me they pleased of old, The wiser youngsters of today: —So be it, and fall on! If not, If studious youth no longer crave, His ancient appetites forgot, Kingston, or Ballantyne the brave, Or Cooper of the wood and wave: So be it, also! And may I And all my pirates share the grave Where these and their creations lie! </pre> </body> </html> |
TEST
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
@Test public void weCanReadEpub() throws Exception { final Reader reader = book.getSpine().getResource(1).getReader(); final char[] text1 = new char[1000]; reader.read(text1); assertTrue(String.valueOf(text1).startsWith("<?xml version=\"1.0\" encoding=\"utf-8\" ?>")); assertTrue(String.valueOf(text1).contains("the following narrative has been designed, it is now, in return")); assertTrue(String.valueOf(text1).contains("Storm and advent")); assertFalse(String.valueOf(text1).contains("ure, heat and cold,")); final char[] text2 = new char[1000]; reader.read(text2); assertTrue(String.valueOf(text2).startsWith("ure, heat and cold,")); assertTrue(String.valueOf(text2).contains("</html>")); reader.close(); } |
En la línea 4 vemos como estamos cogiendo el recurso 1. Leemos en un buffer de tamaño 1000. Esto sería conveniente que fuera una constante, pero para el ambito de este tutorial es suficiente. Cómo podéis observar estamos leyendo los primeros 1000 bytes o caracteres del archivo 01ZB309.html.
Para comprobar que esto es cierto consideramos que es suficiente ver que empiece como lo hace dicho archivo, que contiene una frase que debería estar en dichos 1000 primeros bytes, y comprobar que contiene la última parte de esos 1000 bytes como se hace en la línea 10. En la línea 12 estamos justamente probando que no contiene parte de lo que debiese estar en los siguientes 1000 bytes, eso forma parte del segundo trozo que queremos leer. Leemos el siguiente trozo y basta con comprobar que empieza justo donde se quedó el anterior y que termina con el cerrado del archivo html como se hace en la línea 17. Por último cerramos el reader.
4.2 Lectura Asíncrona
Si lo que pretendemos es leer partes aisladas de un Epub o posicionarnos dentro de un recurso, EpubLib cuenta con una forma sencilla de hacerlo. Lo vamos a ver al intentar obtener la segunda parte del archivo «01ZB309.html». Lo que queremos obtener es justamente el contenido de text2 del test anterior, pero sin tener que leer text1. El posicionamiento se hace de la siguiente forma:
TEST
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@Test public void weCanPositionInDifferentsBytes() throws Exception { final Reader reader = book.getSpine().getResource(1).getReader(); final char[] text2 = new char[1000]; reader.skip(1000); reader.read(text2); assertTrue(String.valueOf(text2).startsWith("ure, heat and cold,")); assertTrue(String.valueOf(text2).contains("</html>")); reader.close(); } |
Et voilà!! Es tan sencillo como saltarnos los bytes que no nos interesan con la función skip.
5. Conclusiones
Como habéis podido ver EpubLib es una librería muy sencilla, que entre otras muchas cosas nos permite leer de una forma fácil el contenido de los Epub. Ahora os toca a vosotros experimentar con ella un poquito más. Un saludo y espero que os haya sido útil.
6. Información sobre el autor
Alberto Barranco Ramón es Ingeniero Técnico en Informática de Gestión y Graduado en Ingeniería del Software por la Universidad Politécnica de Madrid
Mail: abarranco@autentia.com.
Twitter: @barrancoalberto
Autentia Real Business Solutions S.L. – «Soporte a Desarrollo».