1if not modules then modules = { } end modules ['mtx-epub'] = {
2 version = 1.001,
3 comment = "companion to mtxrun.lua",
4 author = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
5 copyright = "PRAGMA ADE / ConTeXt Development Team",
6 license = "see context related readme files"
7}
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77local format, gsub, find = string.format, string.gsub, string.find
78local concat, sortedhash = table.concat, table.sortedhash
79
80local formatters = string.formatters
81local replacetemplate = utilities.templates.replace
82
83local addsuffix = file.addsuffix
84local nameonly = file.nameonly
85local basename = file.basename
86local pathpart = file.pathpart
87local joinfile = file.join
88local suffix = file.suffix
89local addsuffix = file.addsuffix
90local removesuffix = file.removesuffix
91local replacesuffix = file.replacesuffix
92
93local copyfile = file.copy
94local removefile = os.remove
95
96local needsupdating = file.needsupdating
97
98local isdir = lfs.isdir
99local isfile = lfs.isfile
100local mkdir = lfs.mkdir
101
102local pushdir = dir.push
103local popdir = dir.pop
104
105local helpinfo = [[
106<?xml version="1.0"?>
107<application>
108 <metadata>
109 <entry name="name">mtx-epub</entry>
110 <entry name="detail">ConTeXt EPUB Helpers</entry>
111 <entry name="version">1.10</entry>
112 </metadata>
113 <flags>
114 <category name="basic">
115 <subcategory>
116 <flag name="make"><short>create epub zip file</short></flag>
117 <flag name="purge"><short>remove obsolete files</short></flag>
118 <flag name="rename"><short>rename images to sane names</short></flag>
119 <flag name="svgmath"><short>convert mathml to svg</short></flag>
120 <flag name="svgstyle"><short>use given tex style for svg generation (overloads style in specification)</short></flag>
121 <flag name="all"><short>assume: --purge --rename --svgmath (for fast testing)</short></flag>
122 </subcategory>
123 </category>
124 </flags>
125 <examples>
126 <category>
127 <title>Example</title>
128 <subcategory>
129 <example><command>mtxrun --script epub --make mydocument</command></example>
130 </subcategory>
131 </category>
132 </examples>
133</application>
134]]
135
136local application = logs.application {
137 name = "mtx-epub",
138 banner = "ConTeXt EPUB Helpers 1.10",
139 helpinfo = helpinfo,
140}
141
142local report = application.report
143
144
145
146scripts = scripts or { }
147scripts.epub = scripts.epub or { }
148
149local mimetype = "application/epub+zip"
150
151local t_container = [[
152<?xml version="1.0" encoding="UTF-8"?>
153
154<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
155 <rootfiles>
156 <rootfile full-path="OEBPS/%rootfile%" media-type="application/oebps-package+xml"/>
157 </rootfiles>
158</container>
159]]
160
161
162
163
164
165local t_package = [[
166<?xml version="1.0" encoding="UTF-8"?>
167
168<package xmlns="http://www.idpf.org/2007/opf" unique-identifier="%identifier%" version="3.0">
169
170 <metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf">
171 <dc:title>%title%</dc:title>
172 <dc:language>%language%</dc:language>
173 <dc:identifier id="%identifier%">%uuid%</dc:identifier>
174 <dc:creator>%creator%</dc:creator>
175 <dc:date>%date%</dc:date>
176 <!--
177 <dc:subject>%subject%</dc:subject>
178 <dc:description>%description%</dc:description>
179 <dc:publisher>%publisher%</dc:publisher>
180 <dc:source>%source%</dc:source>
181 <dc:relation>%relation%</dc:relation>
182 <dc:coverage>%coverage%</dc:coverage>
183 <dc:rights>%rights%</dc:rights>
184 -->
185 <meta name="cover" content="%coverpage%" />
186 <meta name="generator" content="ConTeXt MkIV" />
187 <meta property="dcterms:modified">%date%</meta>
188 </metadata>
189
190 <manifest>
191%manifest%
192 </manifest>
193
194 <spine toc="ncx">
195 <itemref idref="cover-xhtml" />
196 <itemref idref="%rootfile%" />
197 </spine>
198
199</package>
200]]
201
202
203local t_item = [[ <item id="%id%" href="%filename%" media-type="%mime%" />]]
204local t_prop = [[ <item id="%id%" href="%filename%" media-type="%mime%" properties="%properties%" />]]
205
206
207
208local t_toc = [[
209<?xml version="1.0" encoding="UTF-8"?>
210
211<!-- this is no longer needed in epub 3.0+ -->
212
213<ncx xmlns="http://www.daisy.org/z3986/2005/ncx/" version="2005-1">
214
215 <head>
216 <meta name="generator" content="ConTeXt MkIV" />
217 <meta name="dtb:uid" content="%identifier%" />
218 <meta name="dtb:depth" content="2" />
219 <meta name="dtb:totalPgeCount" content="0" />
220 <meta name="dtb:maxPageNumber" content="0" />
221 </head>
222
223 <docTitle>
224 <text>%title%</text>
225 </docTitle>
226
227 <docAuthor>
228 <text>%author%</text>
229 </docAuthor>
230
231 <navMap>
232 <navPoint id="np-1" playOrder="1">
233 <navLabel>
234 <text>start</text>
235 </navLabel>
236 <content src="%root%"/>
237 </navPoint>
238 </navMap>
239
240</ncx>
241]]
242
243local t_navtoc = [[
244<?xml version="1.0" encoding="UTF-8"?>
245
246<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops">
247 <head>
248 <meta charset="utf-8" />
249 <title>navtoc</title>
250 </head>
251 <body>
252 <div class="navtoc">
253 <!-- <nav epub:type="lot"> -->
254 <nav epub:type="toc" id="navtoc">
255 <ol>
256 <li><a href="%root%">document</a></li>
257 </ol>
258 </nav>
259 </div>
260 </body>
261</html>
262]]
263
264
265
266
267local t_coverxhtml = [[
268<?xml version="1.0" encoding="UTF-8"?>
269
270<html xmlns="http://www.w3.org/1999/xhtml">
271 <head>
272 <meta charset="utf-8" />
273 <title>cover page</title>
274 </head>
275 <body>
276 <div class="coverpage">
277 %content%
278 </div>
279 </body>
280</html>
281]]
282
283local t_coverimg = [[
284 <img src="%image%" alt="The cover image" style="max-width: 100%%;" />
285]]
286
287
288
289
290
291
292
293
294local function dumbid(filename)
295
296 return nameonly(filename) .. "-" .. suffix(filename)
297end
298
299local mimetypes = {
300 xhtml = "application/xhtml+xml",
301 xml = "application/xhtml+xml",
302 html = "application/html",
303 css = "text/css",
304 svg = "image/svg+xml",
305 png = "image/png",
306 jpg = "image/jpeg",
307 ncx = "application/x-dtbncx+xml",
308 gif = "image/gif",
309
310}
311
312local idmakers = {
313 ncx = function(filename) return "ncx" end,
314
315 default = function(filename) return dumbid(filename) end,
316}
317
318local function relocateimages(imagedata,oldname,newname,subpath,rename)
319 local data = io.loaddata(oldname)
320 if data then
321 subpath = joinfile("..",subpath)
322 report("relocating images")
323 local n = 0
324 local done = gsub(data,[[(id=")(.-)(".-background%-image *: *url%()(.-)(%))]], function(s1,id,s2,name,s3)
325 local data = imagedata[id]
326 if data then
327 local newname = data[id].newname
328 if newname then
329 if subpath then
330 name = joinfile(subpath,basename(newname))
331 else
332 name = basename(newname)
333 end
334
335 end
336 if newname then
337 n = n + 1
338 if rename then
339 name = joinfile(subpath,addsuffix(id,suffix(name)))
340 end
341 return s1 .. id .. s2 .. name .. s3
342 end
343 end
344 end)
345 report("%s images relocated in %a",n,newname)
346 if newname then
347 io.savedata(newname,done)
348 end
349 end
350 return images
351end
352
353function reportobsolete(oldfiles,newfiles,purge)
354
355 for i=1,#oldfiles do oldfiles[i] = gsub(oldfiles[i],"^[%./]+","") end
356 for i=1,#newfiles do newfiles[i] = gsub(newfiles[i],"^[%./]+","") end
357
358 local old = table.tohash(oldfiles)
359 local new = table.tohash(newfiles)
360 local done = false
361
362 for name in sortedhash(old) do
363 if not new[name] then
364 if not done then
365 report()
366 if purge then
367 report("removing obsolete files:")
368 else
369 report("obsolete files:")
370 end
371 report()
372 done = true
373 end
374 report(" %s",name)
375 if purge then
376 removefile(name)
377 end
378 end
379 end
380
381 if done then
382 report()
383 end
384
385 return done
386
387end
388
389
390local zippers = {
391 {
392 name = "zip",
393 binary = "zip",
394 uncompressed = "zip %s -X -0 %s",
395 compressed = "zip %s -X -9 -r %s",
396 },
397 {
398 name = "7z (7zip)",
399 binary = "7z",
400 uncompressed = "7z a -tzip -mx0 %s %s",
401 compressed = "7z a -tzip %s %s",
402 },
403}
404
405function scripts.epub.make(purge,rename,svgmath,svgstyle)
406
407
408
409
410 local filename = environment.files[1]
411
412 if not filename or filename == "" or type(filename) ~= "string" then
413 report("provide filename")
414 return
415 end
416
417 local specpath, specname, specfull
418
419 if isdir(filename) then
420 specpath = filename
421 specname = addsuffix(specpath,"lua")
422 specfull = joinfile(specpath,specname)
423 end
424
425 if not specfull or not isfile(specfull) then
426 specpath = filename .. "-export"
427 specname = addsuffix(filename .. "-pub","lua")
428 specfull = joinfile(specpath,specname)
429 end
430
431 if not specfull or not isfile(specfull) then
432 report("unknown specificaton file %a for %a",specfull or "?",filename)
433 return
434 end
435
436 local specification = dofile(specfull)
437
438 if not specification or not next(specification) then
439 report("invalid specificaton file %a",specfile)
440 return
441 end
442
443 report("using specification file %a",specfull)
444
445
446
447 local defaultcoverpage = "cover.xhtml"
448
449 local name = specification.name or nameonly(filename)
450 local identifier = specification.identifier or ""
451 local htmlfiles = specification.htmlfiles or { }
452 local styles = specification.styles or { }
453 local images = specification.images or { }
454 local htmlroot = specification.htmlroot or htmlfiles[1] or ""
455 local language = specification.language or "en"
456 local creator = specification.creator or "context mkiv"
457 local author = specification.author or "anonymous"
458 local title = specification.title or name
459 local subtitle = specification.subtitle or ""
460 local imagefile = specification.imagefile or ""
461 local imagepath = specification.imagepath or "images"
462 local stylepath = specification.stylepath or "styles"
463 local coverpage = specification.firstpage or defaultcoverpage
464
465 if type(svgstyle) == "string" and not svgstyle then
466 svgstyle = specification.svgstyle or ""
467 end
468
469 local obsolete = false
470
471 if #htmlfiles == 0 then
472 report("no html files specified")
473 return
474 end
475 if htmlroot == "" then
476 report("no html root file specified")
477 return
478 end
479
480 if subtitle ~= "" then
481 title = format("%s, %s",title,subtitle)
482 end
483
484 local htmlsource = specpath
485 local imagesource = joinfile(specpath,imagepath)
486 local stylesource = joinfile(specpath,stylepath)
487
488
489
490
491
492
493
494
495
496
497
498
499
500 local pdftosvg = os.which("mutool") and formatters[ [[mutool draw -o "%s" "%s" %s]] ]
501
502 local f_svgpage = formatters["%s-page-%s.svg"]
503 local f_svgname = formatters["%s.svg"]
504
505 local notupdated = 0
506 local updated = 0
507 local skipped = 0
508 local oldfiles = dir.glob(file.join(imagesource,"*"))
509 local newfiles = { }
510
511 if not pdftosvg then
512 report("the %a binary is not present","mutool")
513 end
514
515
516
517 if not coverpage then
518 report("no cover page (image) defined")
519 elseif suffix(coverpage) ~= "xhtml" then
520 report("using cover page %a",coverpage)
521 local source = coverpage
522 local target = joinfile(htmlsource,coverpage)
523 htmlfiles[#htmlfiles+1 ] = coverpage
524 report("copying coverpage %a to %a",source,target)
525 copyfile(source,target)
526 elseif isfile(coverpage) then
527 report("using cover page image %a",coverpage)
528 images.cover = {
529 height = "100%",
530 width = "100%",
531 page = "1",
532 name = url.filename(coverpage),
533 used = coverpage,
534 }
535 local data = replacetemplate(t_coverxhtml, {
536 content = replacetemplate(t_coverimg, {
537 image = coverpage,
538 })
539 })
540 coverpage = defaultcoverpage
541 local target = joinfile(htmlsource,coverpage)
542 report("saving coverpage to %a",target)
543 io.savedata(target,data)
544 htmlfiles[#htmlfiles+1 ] = coverpage
545 else
546 report("cover page image %a is not present",coverpage)
547 coverpage = false
548 end
549
550 if not coverpage then
551 local data = replacetemplate(t_coverxhtml, {
552 content = "no cover page"
553 })
554 coverpage = defaultcoverpage
555 local target = joinfile(htmlsource,coverpage)
556 report("saving dummy coverpage to %a",target)
557 io.savedata(target,data)
558 htmlfiles[#htmlfiles+1 ] = coverpage
559 end
560
561 for id, data in sortedhash(images) do
562 local name = url.filename(data.name)
563 local used = url.filename(data.used)
564 local base = basename(used)
565 local page = tonumber(data.page) or 1
566
567 if suffix(used) == "pdf" then
568
569 if page > 1 then
570 name = f_svgpage(nameonly(name),page)
571 else
572 name = f_svgname(nameonly(name))
573 end
574 local source = used
575 local target = joinfile(imagesource,name)
576 if needsupdating(source,target) then
577 if pdftosvg then
578 local command = pdftosvg(target,source,page)
579 report("running command %a",command)
580 os.execute(command)
581 updated = updated + 1
582 else
583 skipped = skipped + 1
584 end
585 else
586 notupdated = notupdated + 1
587 end
588 newfiles[#newfiles+1] = target
589 else
590 name = basename(used)
591 local source = used
592 local target = joinfile(imagesource,name)
593 if needsupdating(source,target) then
594 report("copying %a to %a",source,target)
595 copyfile(source,target)
596 updated = updated + 1
597 else
598 notupdated = notupdated + 1
599
600 end
601 newfiles[#newfiles+1] = target
602 end
603 local target = newfiles[#newfiles]
604 if suffix(target) == "svg" and isfile(target) then
605 local data = io.loaddata(target)
606 if data then
607 local done = gsub(data,"<!(DOCTYPE.-)>","<!-- %1 -->",1)
608 if data ~= done then
609 report("doctype fixed in %a",target)
610 io.savedata(target,data)
611 end
612 end
613 end
614 data.newname = name
615 end
616
617 report("%s images checked, %s updated, %s kept, %s skipped",updated + notupdated + skipped,updated,notupdated,skipped)
618
619 if reportobsolete(oldfiles,newfiles,purge) then
620 obsolete = true
621 end
622
623
624
625 local uuid = format("urn:uuid:%s",os.uuid(true))
626 local identifier = "bookid"
627
628 local epubname = removesuffix(name)
629 local epubpath = name .. "-epub"
630 local epubfile = replacesuffix(name,"epub")
631 local epubroot = replacesuffix(name,"opf")
632 local epubtoc = "toc.ncx"
633 local epubmimetypes = "mimetype"
634 local epubcontainer = "container.xml"
635 local epubnavigator = "nav.xhtml"
636
637 local metapath = "META-INF"
638 local datapath = "OEBPS"
639
640 local oldfiles = dir.glob(file.join(epubpath,"**/*"))
641 local newfiles = { }
642
643 report("creating paths in tree %a",epubpath)
644
645 if not isdir(epubpath) then
646 mkdir(epubpath)
647 end
648 if not isdir(epubpath) then
649 report("unable to create path %a",epubpath)
650 return
651 end
652
653 local metatarget = joinfile(epubpath,metapath)
654 local htmltarget = joinfile(epubpath,datapath)
655 local styletarget = joinfile(epubpath,datapath,stylepath)
656 local imagetarget = joinfile(epubpath,datapath,imagepath)
657
658 mkdir(metatarget)
659 mkdir(htmltarget)
660 mkdir(styletarget)
661 mkdir(imagetarget)
662
663 local used = { }
664 local notupdated = 0
665 local updated = 0
666
667 local oldimagespecification = joinfile(htmlsource,imagefile)
668 local newimagespecification = joinfile(htmltarget,imagefile)
669
670 report("removing %a",newimagespecification)
671
672
673 local function registerone(path,filename,mathml)
674 local suffix = suffix(filename)
675 local mime = mimetypes[suffix]
676 if mime then
677 local idmaker = idmakers[suffix] or idmakers.default
678 local fullname = path and joinfile(path,filename) or filename
679 if mathml then
680 used[#used+1] = replacetemplate(t_prop, {
681 id = idmaker(filename),
682 filename = fullname,
683 mime = mime,
684 properties = "mathml",
685 } )
686 else
687 used[#used+1] = replacetemplate(t_item, {
688 id = idmaker(filename),
689 filename = fullname,
690 mime = mime,
691 } )
692 end
693 return true
694 end
695 end
696
697 local function registerandcopyfile(check,path,name,sourcepath,targetpath,newname,image)
698
699 if name == "" then
700 report("ignoring unknown image")
701 return
702 end
703
704 if newname then
705 newname = replacesuffix(newname,suffix(name))
706 else
707 newname = name
708 end
709
710 local source = joinfile(sourcepath,name)
711 local target = joinfile(targetpath,newname)
712 local mathml = false
713
714 if suffix(source) == "xhtml" then
715 if find(io.loaddata(source),"MathML") then
716 mathml = true
717 end
718 else
719 report("checking image %a -> %a",source,target)
720 end
721 if registerone(path,newname,mathml) then
722 if not check or needsupdating(source,target) or mathml and svgmath then
723 report("copying %a to %a",source,target)
724 copyfile(source,target)
725 updated = updated + 1
726 else
727 notupdated = notupdated + 1
728 end
729 newfiles[#newfiles+1] = target
730 if mathml and svgmath then
731 report()
732 report("converting mathml into svg in %a",target)
733 report()
734 local status, total, unique = moduledata.svgmath.convert(target,svgstyle)
735 report()
736 if status then
737 report("%s formulas converted, %s are unique",total,unique)
738 else
739 report("warning: %a in %a",total,target)
740 end
741 report()
742 end
743 end
744 end
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763 for image, data in sortedhash(images) do
764
765
766
767 registerandcopyfile(true,imagepath,data.newname,imagesource,imagetarget,rename and image,true)
768 end
769 for i=1,#styles do
770 registerandcopyfile(false,stylepath,styles[i],stylesource,styletarget)
771 end
772 for i=1,#htmlfiles do
773 registerandcopyfile(false,false,htmlfiles[i],htmlsource,htmltarget)
774 end
775
776 relocateimages(images,oldimagespecification,oldimagespecification,imagepath,rename)
777 relocateimages(images,oldimagespecification,newimagespecification,imagepath,rename)
778
779 report("%s files registered, %s updated, %s kept",updated + notupdated,updated,notupdated)
780
781 local function saveinfile(what,name,data)
782 report("saving %s in %a",what,name)
783 io.savedata(name,data)
784 newfiles[#newfiles+1] = name
785 end
786
787 used[#used+1] = replacetemplate(t_prop, {
788 id = "nav",
789 filename = epubnavigator,
790 properties = "nav",
791 mime = "application/xhtml+xml",
792 })
793
794 registerone(false,epubtoc)
795
796 saveinfile("navigation data",joinfile(htmltarget,epubnavigator),replacetemplate(t_navtoc, {
797 root = htmlroot,
798 } ) )
799
800 saveinfile("used mimetypes",joinfile(epubpath,epubmimetypes),mimetype)
801
802 saveinfile("version 2.0 container",joinfile(metatarget,epubcontainer),replacetemplate(t_container, {
803 rootfile = epubroot
804 } ) )
805
806 local idmaker = idmakers[suffix(htmlroot)] or idmakers.default
807
808 saveinfile("package specification",joinfile(htmltarget,epubroot),replacetemplate(t_package, {
809 identifier = identifier,
810 title = title,
811 language = language,
812 uuid = uuid,
813 creator = creator,
814 date = os.date("!%Y-%m-%dT%H:%M:%SZ"),
815 coverpage = idmaker(coverpage),
816 manifest = concat(used,"\n"),
817 rootfile = idmaker(htmlroot)
818 } ) )
819
820
821
822 saveinfile("table of contents",joinfile(htmltarget,epubtoc), replacetemplate(t_toc, {
823 identifier = uuid,
824 title = title,
825 author = author,
826 root = htmlroot,
827 } ) )
828
829 report("creating archive\n\n")
830
831 pushdir(epubpath)
832
833 removefile(epubfile)
834
835 local usedzipper = false
836
837 local function zipped(zipper)
838 local ok = os.execute(format(zipper.uncompressed,epubfile,epubmimetypes))
839 if ok == 0 then
840 os.execute(format(zipper.compressed,epubfile,metapath))
841 os.execute(format(zipper.compressed,epubfile,datapath))
842 usedzipper = zipper.name
843 return true
844 end
845 end
846
847
848
849 for i=1,#zippers do
850 if os.which(zippers[i].binary) and zipped(zippers[i]) then
851 break
852 end
853 end
854
855
856
857 if not usedzipper then
858 for i=1,#zippers do
859 if zipped(zippers[i]) then
860 break
861 end
862 end
863 end
864
865 popdir()
866
867 if usedzipper then
868 local treefile = joinfile(epubpath,epubfile)
869 removefile(epubfile)
870 copyfile(treefile,epubfile)
871 if isfile(epubfile) then
872 removefile(treefile)
873 end
874 report("epub archive made using %s: %s",usedzipper,epubfile)
875 else
876 local list = { }
877 for i=1,#zippers do
878 list[#list+1] = zippers[i].name
879 end
880 report("no epub archive made, install one of: % | t",list)
881 end
882
883 if reportobsolete(oldfiles,newfiles,purge) then
884 obsolete = true
885 end
886
887 if obsolete and not purge then
888 report("use --purge to remove obsolete files")
889 end
890
891end
892
893
894
895local a_exporthelp = environment.argument("exporthelp")
896local a_make = environment.argument("make")
897local a_all = environment.argument("all")
898local a_purge = a_all or environment.argument("purge")
899local a_rename = a_all or environment.argument("rename")
900local a_svgmath = a_all or environment.argument("svgmath")
901local a_svgstyle = environment.argument("svgstyle")
902
903if a_make and a_svgmath then
904 require("x-math-svg")
905end
906
907if a_make then
908 scripts.epub.make(a_purge,a_rename,a_svgmath,a_svgstyle)
909elseif a_exporthelp then
910 application.export(a_exporthelp,environment.files[1])
911else
912 application.help()
913end
914
915
916 |