2025年2月2日 星期日

EPUB 3.0版固定配置圖片格式使用說明與實作

有了電子紙閱讀器後,就有想過把一些圖片給包成電子書格式
也有好心的網友給了我這個網址
https://github.com/dpublishing/epub3guide
不過我幾乎是完全忘了這件事,隔了至少有兩年了吧
大過年的放鬆之下,卻突然想起來
那就來搞搞吧
花了4個小時左右,也如願讓我搞定了它

自己想要的功能並不複雜
就是想要包圖片而已,所以這篇並不會詳解所有電子書epub的格式
只有針對第6個範例練習,Fix Layout Image(固定配置圖片)的部分
https://github.com/dpublishing/epub3guide/blob/master/practices/06_Fixed_Layout_Image/
進行電子書格式的大概說明與解說
還有如何更改成自己想要的樣子

首先epub格式跟現在的office文件(docx、xlsx、pptx)相同
都是zip格式的壓縮檔,然後在裡面放置文件設定與指引,還有資源:圖片、聲音與影像等。
在主要格式上,3.0版在壓縮檔內會長成這個樣子:
┬─META-INF/
│   └container.xhtml
├─item/
│   ├其它資料夾/
│   ├navigation-documents.xhtml
│   └standard.opf
└mimetype

根目錄下有兩個主資料夾META-INF、item還有個純文字檔mimetype
其中mimetype裡的資訊只有這一行
「application/epub+zip」
講的很清楚,這個壓縮檔是個epub格式加上zip壓縮。
在META-INF的資料夾下的container.xhtml檔案主要功能是指定最主要的格式檔opf在哪裡
主要內容如下:
<rootfile
 full-path="item/standard.opf"
 media-type="application/oebps-package+xml"
/>
這邊指定的是item底下的standard.opf,當然也可以改成其它路徑或檔名
像是3.3版的範例是放在OEBPS資料夾下,名稱改為content.opf
副檔名雖然是opf,但這也是個XML檔,也是最主要的設定檔
因為除了各單頁或單章節之外
它幾乎指定所有的東西
以這個範例6來說:
書名,作者名,出版社名,檔案的uuid、檔案製作(成書)時間
書頁的配置格式是固定好的,還是可以浮動的
目錄檔navigation-documents.xhtml的指定,如果嫌navigation-documents.xhtml檔名太長
要改檔名也是在standard.opf裡指定
另外圖片檔存放的位置,與其ID,圖片單頁xhtml的位置。
書是左翻書,還是右翻書,每頁的排列順序。
這頁面是不是封面,是左邊頁面還是右邊頁面。
全部都是這裡決定的
大概詳述一下裡面會用到的東西。

首先最簡單的就是書名、作者與主配置那些東西
放在metadata的標籤裡
書名、作者名那些就不一一說明了,主要說明一下配置的兩個設定
<meta property="rendition:layout">pre-paginated</meta>
<meta property="rendition:spread">landscape</meta>      

rendition:layout有兩個值可用,一個就是現在用的,固定式的pre-paginated
另一個就是浮動的reflowable
而rendition:spread有四個值可用,分別就是現在橫向時(landscape)顯示兩頁
還有none(不設定),auto(自動決定)與垂直時(portait)顯示兩頁

再來就是manifest標籤裡的元件或是資源位置與id的指定了。
這邊先說明一下「其它資料夾」有哪些
以這個範例來說,就是style、image、xhtml這三個
style裡面放決定版本的css,這個基本上不會變動
image裡就是放圖片,而xhtml裡就是放xhtml,這個後面再說明。
回到manifest裡,範例最先放的是目錄檔的位置與設定
<item media-type="application/xhtml+xml" id="toc" href="navigation-documents.xhtml" properties="nav"/>
這個基本上除了改檔名與id名稱外,大概都不會變動
因為那個properties指定為nav就是說目錄要看這個檔案設定
再來放的是css檔的位置
<item media-type="text/css" id="fixed-layout" href="style/fixed-layout.css"/>
這個大概也只有改檔名或id名稱而已
最後就是這個電子書「看得見」的內容,圖片與指定的頁面
首先圖片設定長這樣
<item media-type="image/jpeg" id="cover"      href="image/cover.jpg" property="cover-image"/
<item media-type="image/jpeg" id="i-preface"  href="image/i-preface.jpg"/>
<item media-type="image/jpeg" id="i-001"      href="image/i-001.jpg"/>

蠻簡單的,就是指定類別,id與位置,頂多指定封面是哪張圖
xhtml也是
<item media-type="application/xhtml+xml" id="p-cover"    href="xhtml/p-cover.xhtml"  properties="svg"/>
<item media-type="application/xhtml+xml" id="p-white"    href="xhtml/p-white.xhtml"    properties="svg"/>
<item media-type="application/xhtml+xml" id="p-preface"  href="xhtml/p-preface.xhtml"  properties="svg"/>

同樣是指定類別,id,位置與屬性為svg(一種向量圖形)
格式不難,但這也是製作過程中最麻煩的部分了
因為這範本只有少少的12張圖,如果有200張圖的話,你這邊要生成200*2,共400行出來。
然後接下來用來指定頁面的spine也還要再生出200行出來,共600行
可以的話,還是用程式去產生吧。
manifest的部分就這樣,換spine

spine標籤裡的部分則更複雜了點。
先講spine這標籤如果沒加page-progression-direction="rtl"這個參數
就是一律預設左翻書,也就是大部分橫式(英文)圖書的方向
所以要改成
<spine page-progression-direction="rtl">
這樣之後,才能像一般漫畫與中文小說那樣右翻書
像這個範例6就是沒加,預設為左翻書
再來就是加內容的部分
<itemref linear="yes" idref="p-cover"    properties="rendition:page-spread-center"/>
<itemref linear="yes" idref="p-white"    properties="page-spread-left"/>
<itemref linear="yes" idref="p-preface"  properties="page-spread-right"/>

這邊指定的是id的部分,也就是前面xhtml如果沒有這個id就不會顯示
linear="yes"是指應該要顯示,而no就是不顯示
後面properties可以看到三種不同的參數
第一個rendition:page-spread-center很明顯就是指定橫向兩頁配置要置中
第二個page-spread-left就是指這頁在兩頁中是放左邊
第三個page-spread-right當然就是放右邊
這在搞漫畫時,記得不要搞錯,不然橫著看時,會很奇怪啊
遇上有跨頁更是會顯示異常了
主設定檔的部分就這樣,接下來講一下目錄檔navigation-documents.xhtml

目錄檔的設定也不難,簡單用文字說明就是
用ol條列式標籤,將各列文字加上目標的連結
<nav epub:type="toc" id="toc">
<h1>目錄</h1>
<ol>
<li><a href="xhtml/p-cover.xhtml">封面</a></li>
<li><a href="xhtml/p-preface.xhtml">製作緣起</a></li>
<li><a href="xhtml/p-001.xhtml">內容</a></li>
<li><a href="xhtml/p-colophon.xhtml">版權頁</a></li>
</ol>
</nav>

只是它是包在nav這個標籤中,這可能要注意一下
剩下來其它部分,css就是固定檔不動,image就是放圖檔
格式最後就是各頁xhtml檔的部分了

這邊算簡單,但也很麻煩
詳細看一下下面的全檔內容
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html>
<html
 xmlns="http://www.w3.org/1999/xhtml"
 xmlns:epub="http://www.idpf.org/2007/ops"
 xml:lang="zh-TW" lang="zh-TW">
<head>
<meta charset="UTF-8"/>
<title>
清院本清明上河圖局部</title>
<link rel="stylesheet" type="text/css" href="../style/fixed-layout.css"/>
<meta name="viewport" content="width=
1444, height=2048"/>
<
/head>
<
body epub:type="cover">
<
div class="main">
<
svg xmlns="http://www.w3.org/2000/svg" version="1.1"
 
xmlns:xlink="http://www.w3.org/1999/xlink"
 
width="100%" height="100%" viewBox="0 0 1444 2048">
<
image width="1444" height="2048" xlink:href="../image/cover.jpg"/>
<
/svg>
<
/div>
<
/body>
<
/html>

簡單的部分是因為要改的項目非常明白,就上面標紅色的那些,沒有額外的參數
麻煩的部分就是,要改的項目裡,除了要改的標題,指向檔名的置換外
還一項目是要填圖片的大小,還散在三個地方
分別是viewport,viewBox與image裡的width跟height
所以手動改的時候不能直看檔名填資料,還要開圖看大小
而且一張圖一個檔案,除了如前面所說要在standard.opf裡加上這資料夾檔外產生好幾行外
手動還要一個檔案,一個檔案輸入這些資訊? 
嗯,算了吧,還是靠程式生成比較快,最後面再來談這部分

到這裡,整個格式就講完了
接著只要壓縮,改副檔名就可以讀了
但自己壓的時候要注意,壓縮檔的根目錄一定要有META-INF跟mimetype這兩項
一開始我傻傻地直接對06_Fixed_Layout_Image資料夾壓
結果畫面跑不出來,因為多包了一層06_Fixed_Layout_Image的資料夾
當然如果使用官方工具EPUBCheck就沒這個問題了
不過大概是覺得壓個壓縮檔沒有困難,就這樣掉進了一格洞中

最後來談談生成的程式,或者說步驟
目前採取的生成策略很簡單,並不是全靠程式憑空生出格式
而是先用範例6改出一個幾乎是空的範本目錄,裡面保留不改的檔案是這三個
mimetype,container.xhtml,fixed-layout.css
navigation-documents.xhtml的內容只留下封面跟目錄這兩行,其它刪掉
(目錄可以改為內容)
然後,standard.opf刪掉圖片與頁面的資料,改名為template.opf
xhtml裡留下p-cover.xhtml改名template.xhtml
然後把整個資料夾改名為template
接著才是程式接手的地方

程式先讀取或設定好書名資訊,來源資料夾,目標資料夾後
將template資料夾複製為新的目標資料夾
再把圖片從來源端複製到目標資料夾\item\image\底下
最後就是重頭戲,XML的生成與更改
也是這次寫生成器遇到問題最多的,之前沒碰過XML的格式沒經驗嘛
所以本來我是想用純文字檔讀寫的方式硬幹啦
後來想想,都有了XML這麼明確的格式了,不然請ChatGPT幫我寫程式
把這個XML內容丟進去,然後說明要替換或新增哪些項目後
ChatGPT就生成了90%能用的範例出來,真是AI萬歲
要是之前自己寫,大概要花上個一週讀那些函式庫與上stack overflow看人家的範例
順便再問網友才有辦法寫得出來吧

不過完成度還是沒有100%,AI生出來的程式有遇到三個問題
第一個,改出來的XML檔,在標籤前會多了個ns0的贅字
舉例來說就是<meta>會變成<ns0:meta>
我是沒有試過放這樣的XML進epub裡會不會怎樣啦
但跟範例不同就是覺得不對,得改掉
第二個,在改頁面的xhtml檔時,搜尋不到svg節點
就是本來用find或findall函數,可以找得到其它像title,meta,itemref之類的標籤節點
可是頁面XML檔就是找不到svg這個節點
第三個問題,是新增的元件間沒有換行
這是在改standard.opf檔加圖片與頁面資料時會遇到的
也就是產生的資料會像這樣<item id='01'><item id='02'><item id='03'>連在一起
這個實際上不影響電子書所有功能
只是開檔案要用人工檢查時很不方便而已
還是修掉比較好

那三個問題的解法如下程式:

#解第一個問題,用lxml替換掉原生xml函式庫
#import xml.etree.ElementTree as ET
import lxml.etree as ET
#原生xml函式庫對namespace的支援性不好
#會自己產生ns0字串,要替換成lxml才不會有問題

#載入XML檔
tree = ET.parse('template.opf')
#取得根節點
root = tree.getroot()

#指定namespace map
#原範例是 ns = {'opf': 'http://www.idpf.org/2007/opf', 
#             'dc': 'http://purl.org/dc/elements/1.1/'}
#但這邊只用了opf與dc兩個namespace map
#svg標籤用的是另一個namespace,所以要補上
#這樣就解了第二個找不到svg資料的問題
ns = {'opf': 'http://www.idpf.org/2007/opf', 
    'dc': 'http://purl.org/dc/elements/1.1/', 
    'svg' : 'http://www.w3.org/2000/svg'}

#從根節點取得spine節點
spine = root.find('://opf:spine',ns)
#將變數填入資料
item_id = 'P-Cover'
properties = "rendition:page-spread-center"
#產生節點內的新資料
ET.SubElement(spine, "{http://www.idpf.org/2007/opf}itemref", {
    "linear": "yes",
    "idref": item_id,
    "properties": properties
    }).tail ='\n'#解第三個問題,在最後加上.tail,並填入'\n'後,就會換行了
    
#或是這樣處理,產生新的資料先丟到itemref
itemref = ET.SubElement(spine, "{http://www.idpf.org/2007/opf}itemref", {
    "linear": "yes",
    "idref": item_id,
    "properties": properties
    })
#然後在itemref後加入.tail='\n'
#也就是在這資料的結尾加上換行符號
itemref.tail = '\n'

#修正這三個問題後,輸出到新的目標
tree.write('fix.opf', encoding="UTF-8", xml_declaration=True)
#結束

知道之後是很簡單啦
不過我也是與之奮戰了好幾個小時才搞定
我真的與XML不熟,誰知道那個namespace會影響那麼多東西
這樣又完成了一件想做的事了
耶!

沒有留言: