summaryrefslogtreecommitdiffhomepage
path: root/dlang/src/ocda/images.d
blob: b3a965260e526db776536ca71a5dac60539f10bb (plain)
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_;
}