1
2
3
4
5
6
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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
|
//! Package transformed XHTML content into a valid EPUB3 file (a ZIP with a
//! specific structure). The `mimetype` entry must be first and stored uncompressed.
const std = @import("std");
const assert = std.debug.assert;
const zip = @import("zip.zig");
/// Fixed modification timestamp for reproducible output.
const modified = "2021-01-01T00:00:00Z";
pub const Options = struct {
title: []const u8,
/// Version name, used in the title and as the unique identifier basis.
version: []const u8,
lang: []const u8 = "en",
/// Well-formed XHTML5 body (output of html.toXhtml).
xhtml: []const u8,
};
/// Build an EPUB3 archive from `opts`. Caller owns the returned bytes.
pub fn build(allocator: std.mem.Allocator, opts: Options) ![]u8 {
assert(opts.version.len > 0);
assert(opts.xhtml.len > 0);
var w = zip.Writer.init(allocator);
defer w.deinit();
// 1. mimetype — MUST be the first entry and stored uncompressed.
try w.addStored("mimetype", "application/epub+zip");
// 2. container.xml — points the reader at the package document.
try w.addStored("META-INF/container.xml", container_xml);
// 3. content.opf — package metadata, manifest, spine.
const opf = try buildOpf(allocator, opts);
defer allocator.free(opf);
try w.addStored("OEBPS/content.opf", opf);
// 4. nav.xhtml — minimal EPUB3 navigation document.
const nav = try buildNav(allocator, opts);
defer allocator.free(nav);
try w.addStored("OEBPS/nav.xhtml", nav);
// 5. index.xhtml — the transformed reference content.
try w.addStored("OEBPS/index.xhtml", opts.xhtml);
const bytes = try w.finish();
assert(std.mem.startsWith(u8, bytes, "PK"));
return bytes;
}
const container_xml =
\\<?xml version="1.0" encoding="utf-8"?>
\\<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
\\ <rootfiles>
\\ <rootfile full-path="OEBPS/content.opf" media-type="application/oebps-package+xml"/>
\\ </rootfiles>
\\</container>
\\
;
fn buildOpf(allocator: std.mem.Allocator, opts: Options) ![]u8 {
return std.fmt.allocPrint(allocator,
\\<?xml version="1.0" encoding="utf-8"?>
\\<package xmlns="http://www.idpf.org/2007/opf" version="3.0" unique-identifier="pub-id">
\\ <metadata xmlns:dc="http://purl.org/dc/elements/1.1/">
\\ <dc:identifier id="pub-id">urn:ziglang-docs:{s}</dc:identifier>
\\ <dc:title>{s} ({s})</dc:title>
\\ <dc:language>{s}</dc:language>
\\ <meta property="dcterms:modified">{s}</meta>
\\ </metadata>
\\ <manifest>
\\ <item id="nav" href="nav.xhtml" media-type="application/xhtml+xml" properties="nav"/>
\\ <item id="index" href="index.xhtml" media-type="application/xhtml+xml"/>
\\ </manifest>
\\ <spine>
\\ <itemref idref="index"/>
\\ </spine>
\\</package>
\\
, .{ opts.version, opts.title, opts.version, opts.lang, modified });
}
fn buildNav(allocator: std.mem.Allocator, opts: Options) ![]u8 {
return std.fmt.allocPrint(allocator,
\\<?xml version="1.0" encoding="utf-8"?>
\\<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" lang="{s}">
\\ <head><title>{s} ({s})</title></head>
\\ <body>
\\ <nav epub:type="toc" id="toc">
\\ <h1>Contents</h1>
\\ <ol>
\\ <li><a href="index.xhtml">{s} ({s})</a></li>
\\ </ol>
\\ </nav>
\\ </body>
\\</html>
\\
, .{ opts.lang, opts.title, opts.version, opts.title, opts.version });
}
test "epub starts with PK and mimetype, and round-trips" {
const gpa = std.testing.allocator;
const bytes = try build(gpa, .{
.title = "Zig Language Reference",
.version = "0.15.2",
.xhtml = "<?xml version=\"1.0\"?>\n<html xmlns=\"http://www.w3.org/1999/xhtml\"><body>hi</body></html>",
});
defer gpa.free(bytes);
try std.testing.expect(std.mem.startsWith(u8, bytes, "PK"));
// mimetype must be the first entry, stored, with its content right after the
// local header + filename (offset 30 + len("mimetype") == 38).
try std.testing.expectEqualStrings("mimetype", bytes[30..38]);
try std.testing.expectEqualStrings("application/epub+zip", bytes[38..58]);
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
try tmp.dir.writeFile(.{ .sub_path = "b.epub", .data = bytes });
var file = try tmp.dir.openFile("b.epub", .{});
defer file.close();
var buf: [4096]u8 = undefined;
var fr = file.reader(&buf);
try std.zip.extract(tmp.dir, &fr, .{});
const opf = try tmp.dir.readFileAlloc(gpa, "OEBPS/content.opf", 1 << 16);
defer gpa.free(opf);
try std.testing.expect(std.mem.indexOf(u8, opf, "0.15.2") != null);
}
|