Generar un EPUB PROFESIONAL en Laravel o cualquier aplicación en PHP (CodeIgniter)

Video thumbnail

Voy a explicarte, de manera resumida, cómo puedes generar un archivo EPUB (libro en formato EPUB) con tu proyecto en Laravel. Realmente, estos mismos pasos los puedes aplicar a cualquier proyecto en PHP, incluso si es código nativo. Así que todo este código puedes adaptarlo directamente a PHP, ya que, en un 90%, Laravel no tiene nada que ver aquí. No estamos usando nada propio de Laravel como tal.

La parte que sí se relaciona con Laravel es simplemente para devolver los controladores, modelos y poco más. Toda la integración restante la puedes adaptar perfectamente a cualquier otro entorno.

Vista general de los libros y secciones

Voy a mostrarte cómo funciona. Aquí tengo mis libros —como te decía, es un código que ya tengo trabajado. Cada libro tiene secciones, que puedes ver por acá:

class BookSection extends Model
{
    use HasFactory;

    protected $fillable = ['title', 'description', 'orden', 'content', 'posted', 'book_id', 'content_render'];

    public function book()
    {
        return $this->belongsTo(Book::class);
    }
}   

Proceso de renderizado previo

Este proceso me permite hacer lo que yo llamo un prerenderizado. Por ejemplo, para utilizar resaltado de código (highlight.js), necesito que la página se ejecute primero en el cliente para visualizar correctamente el contenido, y ese es el contenido que guardo en cada sección.

No modifico el contenido original. Guardo este contenido ya procesado en un campo oculto que llamo content_render, donde almaceno el HTML completo dividido por bloques. Al tenerlo dividido así, puedo iterarlo fácilmente. Cada bloque corresponde a un capítulo, lo que me permite procesarlo de forma independiente.

Esto es crucial porque necesito que el código esté bien formateado, especialmente en etiquetas <pre>.

Diferencias entre PDF y EPUB

Aquí surge una diferencia importante:

  1. Para PDF, necesito un único documento HTML, que se convierte en PDF directamente, antes, vimos como generar un PDF con Laravel y DoomPDF.
  2. Para EPUB, quiero dividir el contenido en capítulos, lo cual es lo tradicional, y además el paquete que uso lo permite. Cada capítulo es un documento independiente.

Generar el EPUB desde PHP

No voy a entrar mucho en detalle aquí, porque esta parte es algo más “a mi manera”. Lo explicaré mejor en el curso. Pero en resumen, lo que hago es componer manualmente cada capítulo. Por ejemplo, tengo una sección de introducción que incluye una imagen (que agrego desde PHP) y un título.

Voy tomando la etiqueta <head> de alguna de las secciones y, con eso, armo manualmente el HTML de cada capítulo, que luego agrego a la estructura final del EPUB.

Las imágenes necesitan tener ruta relativa en EPUB, a diferencia del PDF que usa rutas absolutas. También le doy formato a las imágenes, y otras cositas que voy ajustando según detecto necesidades.

Para cada sección, hago una petición con Axios para obtener su contenido renderizado, y cada sección es en sí misma una página HTML independiente. Hay muchas formas de hacerlo, pero yo armé un híbrido para que funcione tanto para PDF como para EPUB.

Lo primero que hago es crear una instancia del objeto EPUB y empiezo a procesar las imágenes de la carátula. Puedes cambiar la extensión .epub a .zip y verás que el EPUB es básicamente un archivo comprimido con carpetas, como una para imágenes.

Por eso, tengo que mover todas las imágenes del proyecto Laravel hacia esa estructura de EPUB que estamos generando. Las rutas se van guardando en la instancia del objeto EPUB.

También agrego metadatos como el título, el autor, etc. Para la imagen de la carátula, guardo tanto en formato WebP como en PNG, ya que algunos visores como el de Apple no soportan WebP y generan errores.

Este tipo de detalles son importantes, sobre todo si planeas distribuir tu EPUB en tiendas como Google Play o Apple Books, que tienen validadores estrictos.

Añado estilos CSS personalizados para títulos, párrafos, código, etc. Todo esto es completamente configurable. Luego genero el nombre del archivo .epub, utilizando el nombre del libro y eliminando caracteres especiales.

Agrego las imágenes al paquete, proceso la introducción manualmente si no es un capítulo separado, y genero el contenido HTML de cada capítulo de manera estructurada.

Cada sección o capítulo debe ser un documento XHTML válido. No sirve un HTML común, porque los lectores EPUB requieren un formato más estricto.

Aquí ajusto la cabecera del documento y añado la declaración correcta de DOCTYPE y de xmlns, porque si no, el visor de Apple, por ejemplo, no lo abrirá correctamente.

Una vez que todos los capítulos están definidos y agregados, genero el archivo EPUB con el método saveBook().

En mi caso, lo almaceno en una carpeta específica del proyecto. Desde la interfaz de mi aplicación, puedo presionar un botón para ejecutar todo este proceso de forma automática. Eso es lo que hace este botón de "Generar".

function generateEpub()
    {
        $pathImages = "/images/example/libros/" . $this->book->path . "/";
        $images = FacadesFile::files(public_path($pathImages));
        $filePath = storage_path('book/');

        // Crear una nueva instancia de EPub
        $epub = new EPub();
        
        //*** */ Configurar los metadatos del libro
        $epub->setTitle($this->book->title);
        $epub->setIdentifier(route('web-book-show', ['post_url_clean' => $this->book->post->url_clean]), EPub::IDENTIFIER_URI);
        $epub->setLanguage($this->book->post->language == 'spanish' ? 'es' : 'en');
        $epub->setDescription($this->book->description);
        $epub->setAuthor('Andrés Cruz Yoris', 'Autor, Andrés Cruz Yoris');
        $epub->setPublisher('DesarrolloLibre', route('web-book-show', ['post_url_clean' => $this->book->post->url_clean]));
        $epub->setRights('Derechos reservados © ' . date('Y') . ' DesarrolloLibre');
        $epub->setDate(time()); // Fecha actual

        //*** Agregar capítulos al libro
        foreach ($this->book->sections()->where('posted', 'yes')->where('orden', '>', 0)->orderBy('orden')->get() as $key => $s) {

            // Acomoda las rutas de las imagenes para que sean relativas
            $html = str_replace("/public/images/example/libros/",   "/images/example/libros/", $s->content_render); // quita el public
            $html = str_replace("/images/example/libros/", "images/example/libros/", $html); // quita un / para que la ruta sea relativa

            // agrega el capitulo al libro
            $epub->addChapter($s->title, "chapter" . $key + 1 . ".xhtml", '<?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, true);
        }

        // Finalizar la creación del ePub
        $epub->finalize();
        // Guardar el archivo ePub en el almacenamiento de Laravel
        $epub->saveBook($fileName, $filePath);

        return response()->download($filePath . $fileName);
    }

Este es el paquete:

https://github.com/Grandt/PHPePub

Visualización del resultado

Al abrir el archivo generado en el visor de Apple, se muestran correctamente la carátula y el índice. También tengo navegación por capítulos y, dependiendo del visor, puede que se vea o no el resaltado de código.

Por ejemplo, Calibre sí muestra el código con estilos, pero el visor de Apple es más limitado en este aspecto. Todo depende de cuánto del CSS interprete el visor.

Acepto recibir anuncios de interes sobre este Blog.

Te explico de manera general, los pasos que debes de seguir para generar un EPUB desde una app Web en PHP.

- Andrés Cruz

In english