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
|
module ocda.images;
/+ ↓ an image's bytes, from whichever source the document came from.
.
The model *names* images, with digest, byte size and pixel size; it does
not hold them. That is right - an abstraction of a document is not a
bundle of its assets - but a writer of any format that shows images
needs the bytes, so something has to know where they are. This is that
something, and it is in the library rather than in a writer because the
answer depends on the *input form* and not on the output.
.
Three answers, and one refusal:
.
.ocda.db the files table, role 'image', by name
.sisupod the archive entry whose path ends in /image/<name>
.zip the same
.ssp beside it: a pod puts the abstraction in
media/abstraction/ and its images in media/image/, so the
path is documented rather than guessed
.
A .ssp that is not in a pod has nowhere to look, and that is reported as
absent rather than as an error: a reader that cannot find an image
should say so and carry on, because the document is still readable.
+/
import std.algorithm : endsWith;
import std.array : array;
import std.file : exists, isFile, read;
import std.path : buildPath, dirName;
import ocda.load;
import ocda.sqlite;
import ocda.zip;
import ocda.zstd;
/+ ↓ the bytes, or an empty slice when the source has no such image +/
ubyte[] imageBytes(string source, string name) {
final switch (sourceOf(source)) {
case Source.ocda_db: return fromDb(source, name);
case Source.pod: return fromArchive(source, name);
case Source.ssp: return fromBeside(source, name);
case Source.unknown: return [];
}
}
/+ ↓ can this source give images at all?
A writer asks before it decides whether to emit them, because an <img>
pointing at a file it did not include is worse than no image.
+/
bool canGiveImages(string source) {
final switch (sourceOf(source)) {
case Source.ocda_db: return true;
case Source.pod: return true;
case Source.ssp: return imageDirBeside(source).length > 0;
case Source.unknown: return false;
}
}
private ubyte[] fromDb(string path, string name) {
auto db = Db.open(path);
scope(exit) { db.close(); }
if (!db.has("files")) { return []; }
/+ ↓ the name is from the abstraction, not from a caller, and the quote
doubling is what keeps it a value rather than syntax +/
return db.blob("SELECT data FROM files WHERE role = 'image' AND name = '"
~ sqlQuote(name) ~ "'");
}
private ubyte[] fromArchive(string path, string name) {
auto bytes = cast(ubyte[]) read(path);
if (zstdIsFrame(bytes)) { bytes = zstdDecompress(bytes, 512 * 1024 * 1024); }
auto entries = zipEntries(bytes);
foreach (e; entries) {
if (e.name.endsWith("/image/" ~ name)) { return zipRead(bytes, e); }
}
return [];
}
private ubyte[] fromBeside(string path, string name) {
string dir = imageDirBeside(path);
if (dir.length == 0) { return []; }
string file = buildPath(dir, name);
return file.exists && file.isFile ? cast(ubyte[]) read(file) : [];
}
/+ ↓ media/abstraction/<x>.ssp -> media/image/ +/
private string imageDirBeside(string path) {
string dir = path.dirName;
if (!dir.endsWith("abstraction")) { return ""; }
string image_dir = buildPath(dir.dirName, "image");
return image_dir.exists ? image_dir : "";
}
/+ ↓ a single quote doubled is how sql says a single quote +/
private string sqlQuote(string s) {
string out_;
foreach (c; s) {
out_ ~= (c == '\'') ? "''" : [c];
}
return out_;
}
|