mtx-epub.lua /size: 32 Kb    last modification: 2025-02-21 11:03
1
 if 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-- The epub specification is far from beautiful. Especially the id related
10-- part is messy and devices/programs react differently on them (so an id is not
11-- really an id but has some special property). Then there is this ncx suffix
12-- thing. Somehow it give the impression of a reversed engineered application
13-- format so it will probably take a few cycles to let it become a real
14-- clean standard. Thanks to Adam Reviczky, Luigi Scarso and Andy Thomas for
15-- helping to figure out all the puzzling details.
16
17-- This is preliminary code. At some point we will deal with images as well but
18-- first we need a decent strategy to export them. More information will be
19-- available on the wiki.
20
21-- META-INF
22--     container.xml
23-- OEBPS
24--     content.opf
25--     toc.ncx
26--     images
27--     styles
28-- mimetype
29
30-- todo:
31--
32-- remove m_k_v_i prefixes
33-- remap fonts %mono% in css so that we can replace
34-- coverpage tests
35-- split up
36
37-- todo: automated cover page:
38--
39-- \startMPpage
40--     StartPage ;
41--         fill Page withcolor .5red ;
42--         numeric n ;
43--         for i=10 downto 1 :
44--             n := i * PaperWidth/40  ;
45--             draw
46--                 lrcorner Page shifted (0,n)
47--               % -- lrcorner Page
48--                 -- lrcorner Page shifted (-n,0)
49--               % -- cycle
50--                 withpen pencircle scaled 1mm
51--                 withcolor white ;
52--         endfor ;
53--         picture p ; p := image (
54--             draw
55--                 anchored.top(
56--                     textext.bot("\tttf Some Title")
57--                         xsized .8PaperWidth
58--                    ,center topboundary Page
59--                 )
60--                     withcolor white ;
61--         ) ;
62--         picture q ; q := image (
63--             draw
64--                 anchored.top(
65--                     textext.bot("\tttf An Author")
66--                         xsized .4PaperWidth
67--                         shifted (0,-PaperHeight/40)
68--                    ,center bottomboundary p
69--                 )
70--                     withcolor white ;
71--         ) ;
72--         draw p ;
73--         draw q ;
74--     StopPage ;
75-- \stopMPpage
76
77local format, gsub, find = string.format, string.gsub, string.find
78local concat, sortedhash = table.concat, table.sortedhash
79
80local formatters      = string.formatters
81local longtostring    = string.longtostring
82local replacetemplate = utilities.templates.replace
83
84local addsuffix       = file.addsuffix
85local nameonly        = file.nameonly
86local basename        = file.basename
87local pathpart        = file.pathpart
88local joinfile        = file.join
89local suffix          = file.suffix
90local addsuffix       = file.addsuffix
91local removesuffix    = file.removesuffix
92local replacesuffix   = file.replacesuffix
93
94local copyfile        = file.copy
95local removefile      = os.remove
96
97local needsupdating   = file.needsupdating
98
99local isdir           = lfs.isdir
100local isfile          = lfs.isfile
101local mkdir           = lfs.mkdir
102
103local pushdir         = dir.push
104local popdir          = dir.pop
105
106local helpinfo = [[
107<?xml version="1.0"?>
108<application>
109 <metadata>
110  <entry name="name">mtx-epub</entry>
111  <entry name="detail">ConTeXt EPUB Helpers</entry>
112  <entry name="version">1.10</entry>
113 </metadata>
114 <flags>
115  <category name="basic">
116   <subcategory>
117    <flag name="make"><short>create epub zip file</short></flag>
118    <flag name="purge"><short>remove obsolete files</short></flag>
119    <flag name="rename"><short>rename images to sane names</short></flag>
120    <flag name="svgmath"><short>convert mathml to svg</short></flag>
121    <flag name="svgstyle"><short>use given tex style for svg generation (overloads style in specification)</short></flag>
122    <flag name="all"><short>assume: --purge --rename --svgmath (for fast testing)</short></flag>
123    <flag name="images"><short>convert images to svg [--fix]</short></flag>
124   </subcategory>
125  </category>
126 </flags>
127 <examples>
128  <category>
129   <title>Example</title>
130   <subcategory>
131    <example><command>mtxrun --script epub --make mydocument</command></example>
132   </subcategory>
133   <subcategory>
134    <example><command>mtxrun --script epub --images mydocument</command></example>
135    <example><command>mtxrun --script epub --images --fix mydocument</command></example>
136   </subcategory>
137  </category>
138 </examples>
139</application>
140]]
141
142local application = logs.application {
143    name     = "mtx-epub",
144    banner   = "ConTeXt EPUB Helpers 1.10",
145    helpinfo = helpinfo,
146}
147
148local report = application.report
149
150-- script code
151
152scripts      = scripts      or { }
153scripts.epub = scripts.epub or { }
154
155local mimetype = "application/epub+zip"
156
157local t_container = [[
158<?xml version="1.0" encoding="UTF-8"?>
159
160<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
161    <rootfiles>
162        <rootfile full-path="OEBPS/%rootfile%" media-type="application/oebps-package+xml"/>
163    </rootfiles>
164</container>
165]]
166
167-- urn:uuid:
168
169-- <dc:identifier id="%identifier%" opf:scheme="UUID">%uuid%</dc:identifier>
170
171local t_package = [[
172<?xml version="1.0" encoding="UTF-8"?>
173
174<package xmlns="http://www.idpf.org/2007/opf" unique-identifier="%identifier%" version="3.0">
175
176    <metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf">
177        <dc:title>%title%</dc:title>
178        <dc:language>%language%</dc:language>
179        <dc:identifier id="%identifier%">%uuid%</dc:identifier>
180        <dc:creator>%creator%</dc:creator>
181        <dc:date>%date%</dc:date>
182        <!--
183            <dc:subject>%subject%</dc:subject>
184            <dc:description>%description%</dc:description>
185            <dc:publisher>%publisher%</dc:publisher>
186            <dc:source>%source%</dc:source>
187            <dc:relation>%relation%</dc:relation>
188            <dc:coverage>%coverage%</dc:coverage>
189            <dc:rights>%rights%</dc:rights>
190        -->
191        <meta name="cover" content="%coverpage%" />
192        <meta name="generator" content="ConTeXt MkIV" />
193        <meta property="dcterms:modified">%date%</meta>
194    </metadata>
195
196    <manifest>
197%manifest%
198    </manifest>
199
200    <spine toc="ncx">
201        <itemref idref="cover-xhtml" />
202        <itemref idref="%rootfile%" />
203    </spine>
204
205</package>
206]]
207
208
209local t_item = [[        <item id="%id%" href="%filename%" media-type="%mime%" />]]
210local t_prop = [[        <item id="%id%" href="%filename%" media-type="%mime%" properties="%properties%" />]]
211
212-- <!DOCTYPE ncx PUBLIC "-//NISO//DTD ncx 2005-1//EN" "http://www.daisy.org/z3986/2005/ncx-2005-1.dtd">
213
214local t_toc = [[
215<?xml version="1.0" encoding="UTF-8"?>
216
217<!-- this is no longer needed in epub 3.0+ -->
218
219<ncx xmlns="http://www.daisy.org/z3986/2005/ncx/" version="2005-1">
220
221    <head>
222        <meta name="generator"         content="ConTeXt MkIV" />
223        <meta name="dtb:uid"           content="%identifier%" />
224        <meta name="dtb:depth"         content="2" />
225        <meta name="dtb:totalPgeCount" content="0" />
226        <meta name="dtb:maxPageNumber" content="0" />
227    </head>
228
229    <docTitle>
230        <text>%title%</text>
231    </docTitle>
232
233    <docAuthor>
234        <text>%author%</text>
235    </docAuthor>
236
237    <navMap>
238        <navPoint id="np-1" playOrder="1">
239            <navLabel>
240                <text>start</text>
241            </navLabel>
242            <content src="%root%"/>
243        </navPoint>
244    </navMap>
245
246</ncx>
247]]
248
249local t_navtoc = [[
250<?xml version="1.0" encoding="UTF-8"?>
251
252<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops">
253    <head>
254        <meta charset="utf-8" />
255        <title>navtoc</title>
256    </head>
257    <body>
258        <div class="navtoc">
259            <!-- <nav epub:type="lot"> -->
260            <nav epub:type="toc" id="navtoc">
261                <ol>
262                    <li><a href="%root%">document</a></li>
263                </ol>
264            </nav>
265        </div>
266    </body>
267</html>
268]]
269
270-- <html xmlns="http://www.w3.org/1999/xhtml">
271-- <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
272
273local t_coverxhtml = [[
274<?xml version="1.0" encoding="UTF-8"?>
275
276<html xmlns="http://www.w3.org/1999/xhtml">
277    <head>
278        <meta charset="utf-8" />
279        <title>cover page</title>
280    </head>
281    <body>
282        <div class="coverpage">
283            %content%
284        </div>
285    </body>
286</html>
287]]
288
289local t_coverimg = [[
290    <img src="%image%" alt="The cover image" style="max-width: 100%%;" />
291]]
292
293-- We need to figure out what is permitted. Numbers only seem to give
294-- problems is some applications as do names with dashes. Also the
295-- optional toc is supposed to be there and although id's are by
296-- concept neutral, there are sometimes hard requirements with respect
297-- to their name like ncx and toc.ncx). Maybe we should stick to 3.0
298-- only.
299
300local function dumbid(filename)
301 -- return (string.gsub(os.uuid(),"%-%","")) -- to be tested
302    return nameonly(filename) .. "-" .. suffix(filename)
303end
304
305local mimetypes = {
306    xhtml   = "application/xhtml+xml",
307    xml     = "application/xhtml+xml",
308    html    = "application/html",
309    css     = "text/css",
310    svg     = "image/svg+xml",
311    png     = "image/png",
312    jpg     = "image/jpeg",
313    ncx     = "application/x-dtbncx+xml",
314    gif     = "image/gif",
315 -- default = "text/plain",
316}
317
318local idmakers = {
319    ncx     = function(filename) return "ncx"            end,
320 -- css     = function(filename) return "stylesheet"     end,
321    default = function(filename) return dumbid(filename) end,
322}
323
324local function relocateimages(imagedata,oldname,newname,subpath,rename)
325    local data = io.loaddata(oldname)
326    if data then
327        subpath = joinfile("..",subpath)
328        report("relocating images")
329        local n = 0
330        local done = gsub(data,[[(id=")(.-)(".-background%-image *: *url%()(.-)(%))]], function(s1,id,s2,name,s3)
331            local data = imagedata[id]
332            if data then
333                local newname = data[id].newname
334                if newname then
335                    if subpath then
336                        name = joinfile(subpath,basename(newname))
337                    else
338                        name = basename(newname)
339                    end
340                 -- name = url.addscheme(name)
341                end
342                if newname then
343                    n = n + 1
344                    if rename then
345                        name = joinfile(subpath,addsuffix(id,suffix(name)))
346                    end
347                    return s1 .. id .. s2 .. name .. s3
348                end
349            end
350        end)
351        report("%s images relocated in %a",n,newname)
352        if newname then
353            io.savedata(newname,done)
354        end
355    end
356    return images
357end
358
359function reportobsolete(oldfiles,newfiles,purge)
360
361    for i=1,#oldfiles do oldfiles[i] = gsub(oldfiles[i],"^[%./]+","") end
362    for i=1,#newfiles do newfiles[i] = gsub(newfiles[i],"^[%./]+","") end
363
364    local old  = table.tohash(oldfiles)
365    local new  = table.tohash(newfiles)
366    local done = false
367
368    for name in sortedhash(old) do
369        if not new[name] then
370            if not done then
371                report()
372                if purge then
373                    report("removing obsolete files:")
374                else
375                    report("obsolete files:")
376                end
377                report()
378                done = true
379            end
380            report("    %s",name)
381            if purge then
382                removefile(name)
383            end
384        end
385    end
386
387    if done then
388        report()
389    end
390
391    return done
392
393end
394
395
396local zippers = {
397    {
398        name         = "zip",
399        binary       = "zip",
400        uncompressed = "zip %s -X -0 %s",
401        compressed   = "zip %s -X -9 -r %s",
402    },
403    {
404        name         = "7z (7zip)",
405        binary       = "7z",
406        uncompressed = "7z a -tzip -mx0 %s %s",
407        compressed   = "7z a -tzip %s %s",
408    },
409}
410
411function scripts.epub.make(purge,rename,svgmath,svgstyle)
412
413    -- one can enter a jobname or jobname-export but the simple jobname is
414    -- preferred
415
416    local filename = environment.files[1]
417
418    if not filename or filename == "" or type(filename) ~= "string" then
419        report("provide filename")
420        return
421    end
422
423    local specpath, specname, specfull
424
425    if isdir(filename) then
426        specpath = filename
427        specname = addsuffix(specpath,"lua")
428        specfull = joinfile(specpath,specname)
429    end
430
431    if not specfull or not isfile(specfull) then
432        specpath = filename .. "-export"
433        specname = addsuffix(filename .. "-pub","lua")
434        specfull = joinfile(specpath,specname)
435    end
436
437    if not specfull or not isfile(specfull) then
438        report("unknown specificaton file %a for %a",specfull or "?",filename)
439        return
440    end
441
442    local specification = dofile(specfull)
443
444    if not specification or not next(specification) then
445        report("invalid specificaton file %a",specfile)
446        return
447    end
448
449    report("using specification file %a",specfull)
450
451    -- images: { ... url = location ... }
452
453    local defaultcoverpage = "cover.xhtml"
454
455    local name       = specification.name       or nameonly(filename)
456    local identifier = specification.identifier or ""
457    local htmlfiles  = specification.htmlfiles  or { }
458    local styles     = specification.styles     or { }
459    local images     = specification.images     or { }
460    local htmlroot   = specification.htmlroot   or htmlfiles[1] or ""
461    local language   = specification.language   or "en"
462    local creator    = specification.creator    or "context mkiv"
463    local author     = specification.author     or "anonymous"
464    local title      = specification.title      or name
465    local subtitle   = specification.subtitle   or ""
466    local imagefile  = specification.imagefile  or ""
467    local imagepath  = specification.imagepath  or "images"
468    local stylepath  = specification.stylepath  or "styles"
469    local coverpage  = specification.firstpage  or defaultcoverpage
470
471    if type(svgstyle) == "string" and not svgstyle then
472        svgstyle = specification.svgstyle or ""
473    end
474
475    local obsolete   = false
476
477    if #htmlfiles == 0 then
478        report("no html files specified")
479        return
480    end
481    if htmlroot == "" then
482        report("no html root file specified")
483        return
484    end
485
486    if subtitle ~= "" then
487        title = format("%s, %s",title,subtitle)
488    end
489
490    local htmlsource  = specpath
491    local imagesource = joinfile(specpath,imagepath)
492    local stylesource = joinfile(specpath,stylepath)
493
494    -- once we're here we can start moving files to the right spot; first we deal
495    -- with images
496
497    -- ["image-1"]={
498    --     height = "7.056cm",
499    --     name   = "file:///t:/sources/cow.svg",
500    --     page   = "1",
501    --     width  = "9.701cm",
502    -- }
503
504    -- end of todo
505
506    local pdftosvg   = os.which("mutool") and formatters[ [[mutool draw -o "%s" "%s" %s]] ]
507
508    local f_svgpage  = formatters["%s-page-%s.svg"]
509    local f_svgname  = formatters["%s.svg"]
510
511    local notupdated = 0
512    local updated    = 0
513    local skipped    = 0
514    local oldfiles   = dir.glob(file.join(imagesource,"*"))
515    local newfiles   = { }
516
517    if not pdftosvg then
518        report("the %a binary is not present","mutool")
519    end
520
521    -- a coverpage file has to be in the root of the export tree
522
523    if not coverpage then
524        report("no cover page (image) defined")
525    elseif suffix(coverpage) ~= "xhtml" then
526        report("using cover page %a",coverpage)
527        local source = coverpage
528        local target = joinfile(htmlsource,coverpage)
529        htmlfiles[#htmlfiles+1 ] = coverpage
530        report("copying coverpage %a to %a",source,target)
531        copyfile(source,target)
532    elseif isfile(coverpage) then
533        report("using cover page image %a",coverpage)
534        images.cover = {
535            height = "100%",
536            width  = "100%",
537            page   = "1",
538            name   = url.filename(coverpage),
539            used   = coverpage,
540        }
541        local data = replacetemplate(t_coverxhtml, {
542            content = replacetemplate(t_coverimg, {
543                image = coverpage,
544            })
545        })
546        coverpage = defaultcoverpage
547        local target = joinfile(htmlsource,coverpage)
548        report("saving coverpage to %a",target)
549        io.savedata(target,data)
550        htmlfiles[#htmlfiles+1 ] = coverpage
551    else
552        report("cover page image %a is not present",coverpage)
553        coverpage = false
554    end
555
556    if not coverpage then
557        local data = replacetemplate(t_coverxhtml, {
558            content = "no cover page"
559        })
560        coverpage = defaultcoverpage
561        local target = joinfile(htmlsource,coverpage)
562        report("saving dummy coverpage to %a",target)
563        io.savedata(target,data)
564        htmlfiles[#htmlfiles+1 ] = coverpage
565    end
566
567    for id, data in sortedhash(images) do
568        local name = url.filename(data.name)
569        local used = url.filename(data.used)
570        local base = basename(used)
571        local page = tonumber(data.page) or 1
572        -- todo : check timestamp and prefix, rename to image-*
573        if suffix(used) == "pdf" then
574            -- todo: pass svg name
575            if page > 1 then
576                name = f_svgpage(nameonly(name),page)
577            else
578                name = f_svgname(nameonly(name))
579            end
580            local source  = used
581            local target  = joinfile(imagesource,name)
582            if needsupdating(source,target) then
583                if pdftosvg then
584                    local command = pdftosvg(target,source,page)
585                    report("running command %a",command)
586                    os.execute(command)
587                    updated = updated + 1
588                else
589                    skipped = skipped + 1
590                end
591            else
592                notupdated = notupdated + 1
593            end
594            newfiles[#newfiles+1] = target
595        else
596            name = basename(used)
597            local source = used
598            local target = joinfile(imagesource,name)
599            if needsupdating(source,target) then
600                report("copying %a to %a",source,target)
601                copyfile(source,target)
602                updated = updated + 1
603            else
604                notupdated = notupdated + 1
605                -- no message
606            end
607            newfiles[#newfiles+1] = target
608        end
609        local target = newfiles[#newfiles]
610        if suffix(target) == "svg" and isfile(target) then
611            local data = io.loaddata(target)
612            if data then
613                local done = gsub(data,"<!(DOCTYPE.-)>","<!-- %1 -->",1)
614                if data ~= done then
615                    report("doctype fixed in %a",target)
616                    io.savedata(target,data)
617                end
618            end
619        end
620        data.newname = name -- without path
621    end
622
623    report("%s images checked, %s updated, %s kept, %s skipped",updated + notupdated + skipped,updated,notupdated,skipped)
624
625    if reportobsolete(oldfiles,newfiles,purge) then
626        obsolete = true
627    end
628
629    -- here we can decide not to make an epub
630
631    local uuid          = format("urn:uuid:%s",os.uuid(true)) -- os.uuid()
632    local identifier    = "bookid" -- for now
633
634    local epubname      = removesuffix(name)
635    local epubpath      = name .. "-epub"
636    local epubfile      = replacesuffix(name,"epub")
637    local epubroot      = replacesuffix(name,"opf")
638    local epubtoc       = "toc.ncx"
639    local epubmimetypes = "mimetype"
640    local epubcontainer = "container.xml"
641    local epubnavigator = "nav.xhtml"
642
643    local metapath      = "META-INF"
644    local datapath      = "OEBPS"
645
646    local oldfiles      = dir.glob(file.join(epubpath,"**/*"))
647    local newfiles      = { }
648
649    report("creating paths in tree %a",epubpath)
650
651    if not isdir(epubpath) then
652        mkdir(epubpath)
653    end
654    if not isdir(epubpath) then
655        report("unable to create path %a",epubpath)
656        return
657    end
658
659    local metatarget  = joinfile(epubpath,metapath)
660    local htmltarget  = joinfile(epubpath,datapath)
661    local styletarget = joinfile(epubpath,datapath,stylepath)
662    local imagetarget = joinfile(epubpath,datapath,imagepath)
663
664    mkdir(metatarget)
665    mkdir(htmltarget)
666    mkdir(styletarget)
667    mkdir(imagetarget)
668
669    local used       = { }
670    local notupdated = 0
671    local updated    = 0
672
673    local oldimagespecification = joinfile(htmlsource,imagefile)
674    local newimagespecification = joinfile(htmltarget,imagefile)
675
676    report("removing %a",newimagespecification)
677 -- removefile(newimagespecification) -- because we update that one
678
679    local function registerone(path,filename,mathml)
680        local suffix = suffix(filename)
681        local mime = mimetypes[suffix]
682        if mime then
683            local idmaker  = idmakers[suffix] or idmakers.default
684            local fullname = path and joinfile(path,filename) or filename
685            if mathml then
686                used[#used+1] = replacetemplate(t_prop, {
687                    id         = idmaker(filename),
688                    filename   = fullname,
689                    mime       = mime,
690                    properties = "mathml",
691                } )
692            else
693                used[#used+1] = replacetemplate(t_item, {
694                    id       = idmaker(filename),
695                    filename = fullname,
696                    mime     = mime,
697                } )
698            end
699            return true
700        end
701    end
702
703    local function registerandcopyfile(check,path,name,sourcepath,targetpath,newname,image)
704
705        if name == "" then
706            report("ignoring unknown image")
707            return
708        end
709
710        if newname then
711            newname = replacesuffix(newname,suffix(name))
712        else
713            newname = name
714        end
715
716        local source = joinfile(sourcepath,name)
717        local target = joinfile(targetpath,newname)
718        local mathml = false
719
720        if suffix(source) == "xhtml" then
721            if find(io.loaddata(source),"MathML") then
722                mathml = true -- inbelievable: the property is only valid when there is mathml
723            end
724        else
725            report("checking image %a -> %a",source,target)
726        end
727        if registerone(path,newname,mathml) then
728            if not check or needsupdating(source,target) or mathml and svgmath then
729                report("copying %a to %a",source,target)
730                copyfile(source,target)
731                updated = updated + 1
732            else
733                notupdated = notupdated + 1
734            end
735            newfiles[#newfiles+1] = target
736            if mathml and svgmath then
737                report()
738                report("converting mathml into svg in %a",target)
739                report()
740                local status, total, unique = moduledata.svgmath.convert(target,svgstyle)
741                report()
742                if status then
743                    report("%s formulas converted, %s are unique",total,unique)
744                else
745                    report("warning: %a in %a",total,target)
746                end
747                report()
748            end
749        end
750    end
751
752 -- local nofdummies = 0
753 -- local dummyname  = formatters["dummy-figure-%03i"]
754 -- local makedummy  = formatters["context --extra=dummies --noconsole --once --result=%s"]
755 --
756 -- local function registerandcopydummy(targetpath,name)
757 --     nofdummies = nofdummies + 1
758 --     local newname = dummyname(nofdummies)
759 --     local target  = joinfile(targetpath,newname)
760 --     if not isfile(target) then
761 --         pushdir(targetpath)
762 --         report("generating dummy %a for %a",newname,name or "unknown")
763 --         os.execute(makedummy(newname))
764 --         popdir()
765 --     end
766 --     return newname
767 -- end
768
769    for image, data in sortedhash(images) do
770     -- if data.used == "" then
771     --     data.newname = registerandcopydummy(imagetarget,data.name)
772     -- end
773        registerandcopyfile(true,imagepath,data.newname,imagesource,imagetarget,rename and image,true)
774    end
775    for i=1,#styles do
776        registerandcopyfile(false,stylepath,styles[i],stylesource,styletarget)
777    end
778    for i=1,#htmlfiles do
779        registerandcopyfile(false,false,htmlfiles[i],htmlsource,htmltarget)
780    end
781
782    relocateimages(images,oldimagespecification,oldimagespecification,imagepath,rename)
783    relocateimages(images,oldimagespecification,newimagespecification,imagepath,rename)
784
785    report("%s files registered, %s updated, %s kept",updated + notupdated,updated,notupdated)
786
787    local function saveinfile(what,name,data)
788        report("saving %s in %a",what,name)
789        io.savedata(name,data)
790        newfiles[#newfiles+1] = name
791    end
792
793    used[#used+1] = replacetemplate(t_prop, {
794        id         = "nav",
795        filename   = epubnavigator,
796        properties = "nav",
797        mime       = "application/xhtml+xml",
798    })
799
800    registerone(false,epubtoc)
801
802    saveinfile("navigation data",joinfile(htmltarget,epubnavigator),replacetemplate(t_navtoc, { -- version 3.0
803        root = htmlroot,
804    } ) )
805
806    saveinfile("used mimetypes",joinfile(epubpath,epubmimetypes),mimetype)
807
808    saveinfile("version 2.0 container",joinfile(metatarget,epubcontainer),replacetemplate(t_container, {
809        rootfile = epubroot
810    } ) )
811
812    local idmaker = idmakers[suffix(htmlroot)] or idmakers.default
813
814    saveinfile("package specification",joinfile(htmltarget,epubroot),replacetemplate(t_package, {
815        identifier = identifier,
816        title      = title,
817        language   = language,
818        uuid       = uuid,
819        creator    = creator,
820        date       = os.date("!%Y-%m-%dT%H:%M:%SZ"),
821        coverpage  = idmaker(coverpage),
822        manifest   = concat(used,"\n"),
823        rootfile   = idmaker(htmlroot)
824    } ) )
825
826    -- t_toc is replaced by t_navtoc in >= 3
827
828    saveinfile("table of contents",joinfile(htmltarget,epubtoc), replacetemplate(t_toc, {
829        identifier = uuid, -- identifier,
830        title      = title,
831        author     = author,
832        root       = htmlroot,
833    } ) )
834
835    report("creating archive\n\n")
836
837    pushdir(epubpath)
838
839    removefile(epubfile)
840
841    local usedzipper = false
842
843    local function zipped(zipper)
844        local ok = os.execute(format(zipper.uncompressed,epubfile,epubmimetypes))
845        if ok == 0 then
846            os.execute(format(zipper.compressed,epubfile,metapath))
847            os.execute(format(zipper.compressed,epubfile,datapath))
848            usedzipper = zipper.name
849            return true
850        end
851    end
852
853    -- nice way
854
855    for i=1,#zippers do
856        if os.which(zippers[i].binary) and zipped(zippers[i]) then
857            break
858        end
859    end
860
861    -- trial and error
862
863    if not usedzipper then
864        for i=1,#zippers do
865            if zipped(zippers[i]) then
866                break
867            end
868        end
869    end
870
871    popdir()
872
873    if usedzipper then
874        local treefile = joinfile(epubpath,epubfile)
875        removefile(epubfile)
876        copyfile(treefile,epubfile)
877        if isfile(epubfile) then
878            removefile(treefile)
879        end
880        report("epub archive made using %s: %s",usedzipper,epubfile)
881    else
882        local list = { }
883        for i=1,#zippers do
884            list[#list+1] = zippers[i].name
885        end
886        report("no epub archive made, install one of: % | t",list)
887    end
888
889    if reportobsolete(oldfiles,newfiles,purge) then
890        obsolete = true
891    end
892
893    if obsolete and not purge then
894        report("use --purge to remove obsolete files")
895    end
896
897end
898
899do
900
901    local runimages = sandbox.registerrunner {
902        name     = "images",
903        program  = "context",
904     -- method   = "command",
905        template = longtostring [[
906            "%fullname%"
907            "--exportimages"
908            "--result=%nameonly%-images"
909        ]],
910    }
911
912    local runconvert = sandbox.registerrunner {
913        name     = "convert",
914        program  = "mutool",
915     -- method   = "command",
916        template = longtostring [[
917            "convert"
918            "-o%imagepath%/%nameonly%-exported-%%d.svg"
919            "%nameonly%-images.pdf"
920        ]],
921    }
922
923    function scripts.epub.images()
924        local files = environment.globfiles()
925        if files then
926
927            statistics.starttiming("export")
928
929            local fix   = environment.argument("fix")
930            local flat  = environment.argument("flat")
931            local path  = environment.argument("path") or "."
932            local jobs  = { }
933            for i=1,#files do
934                local filename = file.basename(files[i])
935                local fullname = file.addsuffix(filename,"tex")
936                local nameonly = file.nameonly(fullname)
937                local pattern  = nameonly .. "-exported-*.svg"
938                local target   = flat and "." or (nameonly .. "-export")
939                if not lfs.isfile(fullname) then
940                    report("no export file %a",fullname)
941                elseif type(path) == "string" and path ~= "" and not lfs.isdir(path) then
942                    report("no export root %a",path)
943                else
944                    local imagepath = file.join(path,target,"images")
945                    dir.makedirs(imagepath)
946                    if lfs.isdir(imagepath) then
947                        jobs[#jobs+1] = {
948                            filename  = filename,
949                            fullname  = fullname,
950                            nameonly  = nameonly,
951                            path      = path,
952                            target    = target,
953                            imagepath = imagepath,
954                            pattern   = file.join(imagepath,pattern),
955                        }
956                    else
957                        report("invalid image path %a",imagepath)
958                    end
959                end
960            end
961
962            local images = 0
963
964            for index=1,#jobs do
965
966                local job = jobs[index]
967
968                -- cleanup
969
970                local files = dir.glob(job.pattern)
971                for i=1,#files do
972                    os.remove(files[i])
973                end
974
975                -- run context
976
977                runimages {
978                    fullname = job.fullname,
979                    nameonly = job.nameonly,
980                }
981
982                -- convert
983
984                runconvert {
985                    nameonly  = job.nameonly,
986                    imagepath = job.imagepath,
987                }
988
989                -- older mutool doesn't turn < > into entities in attributes and
990                -- browsers etc warn but don't recover so for now we have this option
991
992                if fix then -- will go at some point
993                    local files = dir.glob(job.pattern)
994                    for i=1,#files do
995                        local name = files[i]
996                        local data = io.loaddata(name)
997                        if find(data,'"[<>]"') then
998                            report("fixing %a",name)
999                            data = gsub(data,'"([<>])"', { ["<"] = '"&lt;"', [">"] = '"&gt;"' })
1000                            io.savedata(name,data)
1001                        end
1002                    end
1003                end
1004
1005                -- list images
1006
1007                local files = dir.glob(job.pattern)
1008                if #files > 0 then
1009                    images = images + #files
1010                    report()
1011                end
1012                for i=1,#files do
1013                    local name = files[i]
1014                    report("% 7i : %s",file.size(name),file.basename(name))
1015                end
1016
1017            end
1018
1019            statistics.stoptiming("export")
1020
1021            report()
1022            report("%i files, %i images, export time: %s",#jobs,images,statistics.elapsedtime("export"))
1023
1024        end
1025
1026    end
1027
1028end
1029
1030--
1031
1032local a_exporthelp = environment.argument("exporthelp")
1033local a_make       = environment.argument("make")
1034local a_all        = environment.argument("all")
1035local a_purge      = a_all or environment.argument("purge")
1036local a_rename     = a_all or environment.argument("rename")
1037local a_svgmath    = a_all or environment.argument("svgmath")
1038local a_svgstyle   = environment.argument("svgstyle")
1039
1040local a_images     = environment.argument("images")
1041
1042if a_make and a_svgmath then
1043    require("x-math-svg")
1044end
1045
1046if a_images then
1047    scripts.epub.images()
1048elseif a_make then
1049    scripts.epub.make(a_purge,a_rename,a_svgmath,a_svgstyle)
1050elseif a_exporthelp then
1051    application.export(a_exporthelp,environment.files[1])
1052else
1053    application.help()
1054end
1055
1056-- java -jar d:\epubcheck\epubcheck-3.0.1.jar -v 3.0 -mode xhtml mkiv-publications.tree\mkiv-publications.epub
1057