Generate a professional EPUB in Laravel or any PHP application (CodeIgniter)

Video thumbnail

I'm going to briefly explain how you can generate an EPUB file (a book in EPUB format) with your Laravel project. You can actually apply these same steps to any PHP project, even if it's native code. So, you can adapt all this code directly to PHP, since Laravel has nothing to do with it. We're not using anything specific to Laravel itself.

The part that does relate to Laravel is simply to return the controllers, models, and little else. You can perfectly adapt all the remaining integration to any other environment.

Overview of books and sections

I'll show you how it works. Here are my books—as I said, it's a code I've already worked on. Each book has sections, which you can see here:

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);
    }
}   

Pre-rendering process

This process allows me to do what I call pre-rendering. For example, to use code highlighting (highlight.js), I need the page to run first on the client to properly display the content, and that's the content I save in each section.

I don't modify the original content. I save this already rendered content in a hidden field I call content_render, where I store the full HTML divided into blocks. By dividing it this way, I can easily iterate through it. Each block corresponds to a chapter, which allows me to process it independently.

This is crucial because I need the code to be well-formatted, especially in the <pre> tags.

Differences between PDF and EPUB

An important difference arises here:

  1. For PDF, I need a single HTML document, which is converted directly to PDF. We previously saw how to generate a PDF with Laravel and DoomPDF.
  2. For EPUB, I want to divide the content into chapters, which is the traditional approach, and the package I use allows it. Each chapter is a separate document.

Generate the EPUB from PHP

I won't go into much detail here, because this part is more "my way." I'll explain it better in the course. But in short, what I do is manually compose each chapter. For example, I have an introduction section that includes an image (which I add from PHP) and a title.

I take the <head> tag from one of the sections and, with that, manually put together the HTML for each chapter, which I then add to the final EPUB structure.

Images need to have relative paths in EPUB, unlike PDF, which uses absolute paths. I also format the images and adjust other things as needed.

For each section, I make a request with Axios to get its rendered content, and each section is a separate HTML page in itself. There are many ways to do this, but I built a hybrid so it works for both PDF and EPUB.

The first thing I do is create an instance of the EPUB object and start processing the cover images. You can change the .epub extension to .zip, and you'll see that the EPUB is basically a compressed file with folders, like one for images.

Therefore, I need to move all the images from the Laravel project into the EPUB structure we're generating. The paths are saved in the EPUB object instance.

I also add metadata such as the title, author, etc. For the cover image, I save in both WebP and PNG formats, as some viewers, like Apple's, don't support WebP and generate errors.

These types of details are important, especially if you plan to distribute your EPUB on stores like Google Play or Apple Books, which have strict validators.

I add custom CSS styles for headings, paragraphs, code, etc. All of this is completely configurable. Then I generate the .epub filename, using the book name and removing special characters.

I add the images to the package, process the introduction manually if it's not a separate chapter, and generate the structured HTML content for each chapter.

Each section or chapter must be a valid XHTML document. Plain HTML isn't suitable because EPUB readers require stricter formatting.

Here, I adjust the document header and add the correct DOCTYPE and xmlns declaration, because otherwise, the Apple viewer, for example, won't open it correctly.

Once all the chapters are defined and added, I generate the EPUB file using the saveBook() method.

In my case, I store it in a specific folder in the project. From my application interface, I can press a button to run this entire process automatically. That's what this "Generate" button does.

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);
    }

This is the package:

https://github.com/Grandt/PHPePub

Viewing the result

When opening the generated file in Apple's viewer, the cover art and table of contents are displayed correctly. I also have chapter navigation, and depending on the viewer, code highlighting may or may not be visible.

For example, Calibre does display styled code, but Apple's viewer is more limited in this regard. It all depends on how much of the CSS the viewer interprets.

I agree to receive announcements of interest about this Blog.

I'll explain in general terms the steps you need to follow to generate an EPUB from a PHP web app.

- Andrés Cruz

En español