;;; my-epub.el -- epub utils -*- lexical-binding: t -*- ;; Copyright (C) 2025 Free Software Foundation, Inc. ;; Author: Yuchen Pei ;; Package-Requires: ((emacs "30.1")) ;; This file is part of dotted. ;; dotted is free software: you can redistribute it and/or modify it under ;; the terms of the GNU Affero General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or ;; (at your option) any later version. ;; dotted is distributed in the hope that it will be useful, but WITHOUT ;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY ;; or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General ;; Public License for more details. ;; You should have received a copy of the GNU Affero General Public ;; License along with dotted. If not, see . ;;; Commentary: ;; epub utils. ;;; Code: (defun my-epub-content-file-name (file-name) (with-temp-buffer (if (eq 0 (call-process "unzip" nil t nil "-p" file-name "META-INF/container.xml")) (let ((dom (libxml-parse-xml-region (point-min) (point-max)))) (dom-attr (dom-by-tag (dom-by-tag (dom-by-tag dom 'container) 'rootfiles) 'rootfile) 'full-path)) (message "Failed to extract container.xml: %s" (buffer-string)) nil))) (defun my-epub-metadata (file-name) "Get metadata of an epub file." (when-let ((content-file-name (my-epub-content-file-name file-name))) (with-temp-buffer (call-process "unzip" nil t nil "-p" file-name content-file-name) (let* ((dom (libxml-parse-xml-region (point-min) (point-max))) (metadata (dom-by-tag dom 'metadata)) (title (dom-text (dom-by-tag metadata 'title))) (authors (dom-texts (dom-by-tag metadata 'creator) ", ")) (identifier (replace-regexp-in-string "[^0-9,]" "" (dom-texts (seq-filter (lambda (node) (or (equal "ISBN" (dom-attr node 'scheme)) (string-match-p "^[0-9]+$" (dom-text node)))) (dom-by-tag metadata 'identifier)) ","))) (date (replace-regexp-in-string "[^0-9]" "" (dom-text (dom-by-tag metadata 'date)))) (year (substring date 0 (min 4 (length date))))) `((title . ,title) (authors . ,authors) (year . ,year) (identifier . ,identifier)) ;; (pp metadata) )) )) ;; generate epub (defun my-epub-create (dir title author) "Create an epub by concatenating htmls in DIR." (let* ((name (file-name-nondirectory (directory-file-name dir))) (tmpdir (make-temp-file (format "/tmp/%s.epub." name) t)) (files (directory-files dir nil directory-files-no-dot-files-regexp))) (my-epub--create-mimetype tmpdir) (my-epub--create-container tmpdir) (my-epub--create-opf tmpdir files title author) (my-epub--create-toc tmpdir files title) (my-epub--add-html tmpdir (directory-files dir t directory-files-no-dot-files-regexp)) (my-epub--zip tmpdir name) )) (defun my-epub--create-mimetype (dir) (with-temp-buffer (insert "application/epub+zip") (write-file (file-name-concat dir "mimetype")))) (defun my-epub--create-container (dir) (with-temp-buffer (insert " ") (make-directory (file-name-concat dir "META-INF")) (write-file (file-name-concat dir "META-INF/container.xml")))) (defun my-epub--create-opf (dir files title author) (with-temp-buffer (insert "" "\n") (dom-print `(package ((unique-identifier . "BookID") (version . "2.0")) (metadata nil (title nil ,title) (creator nil ,author) (language nil "en") (identifier ((id . "BookID")) "Hello ID")) (manifest nil (item ((id . "ncx") (href . "toc.ncx") (media-type . "application/x-dtbncx+xml"))) ,@(seq-map (lambda (file) `(item ((id . ,file) (href . ,file) (media-type . "application/xhtml+xml")))) files)) (spine ((toc . "ncx")) ,@(seq-map (lambda (file) `(itemref ((idref . ,file)))) files))) t t) (make-directory (file-name-concat dir "OEBPS")) (write-file (file-name-concat dir "OEBPS/content.opf")) (message (buffer-string)))) (defun my-epub--create-toc (dir files title) (with-temp-buffer (insert "" "\n") (dom-print `(ncx nil (docTitle nil (text nil ,title)) (navMap nil ,@(seq-map (lambda (file) `(navPoint nil (content ((src . ,file))))) files))) t t) (write-file (file-name-concat dir "OEBPS/toc.ncx")))) (defun my-epub--add-html (dir files) (seq-do (lambda (file) (copy-file file (file-name-concat dir "OEBPS/"))) files)) (defun my-epub--zip (dir name) (let ((default-directory dir)) (call-process "zip" nil nil nil "-r" (format "/tmp/%s.epub" name) "."))) (provide 'my-epub) ;;; my-epub.el ends here