EpubLib, una librería Java para leer Epub

0
12458

0. Índice de contenidos.

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.

   <?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:

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.


	org.springframework
	spring-context
	3.0.5.RELEASE

 

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:

<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

<?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

@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

@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».

Consultor tecnológico de desarrollo de proyectos informáticos.
Ingeniero técnico en informática de gestión y graduado en ingeniería del software por la Universidad Politécnica de Madrid.
Puedes encontrarme en Autentia: Ofrecemos servicios de soporte a desarrollo, factoría y formación.
Somos expertos en Java/Java EE

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad