more fixes for fb2 idiocy
[xreader.git] / booktext.d
blobe7c216c02e35c56446bc6bc5ff462baee75c0e30
1 /* Written by Ketmar // Invisible Vector <ketmar@ketmar.no-ip.org>
2 * Understanding is not required. Only obedience.
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 module booktext /*is aliced*/;
19 import std.encoding;
21 import arsd.image;
23 import iv.cmdcon;
24 import iv.encoding;
25 //import iv.iresample;
26 import iv.nanovg;
27 import iv.saxy;
28 import iv.strex;
29 import iv.utfutil;
30 import iv.vfs;
32 import xlayouter;
35 // ////////////////////////////////////////////////////////////////////////// //
36 struct BookInfo {
37 string author;
38 string title;
39 string seqname;
40 uint seqnum;
41 string diskfile; // not set by `loadBookInfo()`!
45 BookInfo loadBookInfo (const(char)[] fname) {
46 static BookInfo loadFile (VFile fl) {
47 BookInfo res;
48 auto sax = new SaxyEx();
49 bool complete = false;
50 string authorFirst, authorLast;
52 inout(char)[] strip (inout(char)[] s) inout {
53 while (s.length && s.ptr[0] <= ' ') s = s[1..$];
54 while (s.length && s[$-1] <= ' ') s = s[0..$-1];
55 return s;
58 try {
59 // sequence tag
60 sax.onOpen("/FictionBook/description/title-info/sequence", (char[] text, char[][string] attrs) {
61 if (auto sn = "name" in attrs) {
62 res.seqname = strip(*sn).dup;
63 if (auto id = "number" in attrs) {
64 import std.conv : to;
65 res.seqnum = to!uint(*id);
66 if (res.seqnum < 1 || res.seqnum > 8192) res.seqname = null;
68 } else {
69 res.seqname = null;
71 if (res.seqname.length == 0) { res.seqname = null; res.seqnum = 0; }
72 });
74 sax.onClose("/FictionBook/description", (char[] text) {
75 complete = true;
76 throw new Exception("done"); // sorry
77 });
79 // author's first name
80 sax.onContent("/FictionBook/description/title-info/author/first-name", (char[] text) {
81 authorFirst = strip(text).idup;
82 });
83 // author's last name
84 sax.onContent("/FictionBook/description/title-info/author/last-name", (char[] text) {
85 authorLast = strip(text).idup;
86 });
87 // book title
88 sax.onContent("/FictionBook/description/title-info/book-title", (char[] text) {
89 res.title = strip(text).idup;
90 });
92 sax.loadStream(fl);
93 } catch (Exception e) {
94 if (!complete) throw e;
96 if (authorFirst.length == 0) {
97 authorFirst = authorLast;
98 authorLast = null;
99 } else if (authorLast.length != 0) {
100 authorFirst ~= " "~authorLast;
102 if (authorFirst.length == 0 && res.title.length == 0) throw new Exception("no book title found");
103 res.author = authorFirst;
104 return res;
107 import std.path : extension;
108 if (strEquCI(fname.extension, ".zip")) {
109 auto did = vfsAddPak!"first"(fname);
110 scope(exit) vfsRemovePak(did);
111 foreach (immutable idx, ref de; vfsFileList) {
112 if (strEquCI(de.name.extension, ".fb2")) return loadFile(vfsOpenFile(de.name));
114 throw new Exception("no fb2 file found in '"~fname.idup~"'");
115 } else {
116 return loadFile(vfsOpenFile(fname));
121 // ////////////////////////////////////////////////////////////////////////// //
122 final class Tag {
123 string name; // empty: text node
124 string id;
125 string href;
126 string text;
127 Tag[] children;
128 Tag parent;
129 //Tag prevSibling;
130 //Tag nextSibling;
132 @property inout(Tag) firstChild () inout pure nothrow @trusted @nogc { pragma(inline, true); return (children.length ? children.ptr[0] : null); }
133 @property inout(Tag) lastChild () inout pure nothrow @trusted @nogc { pragma(inline, true); return (children.length ? children[$-1] : null); }
135 string textContent () const {
136 if (name.length == 0) return text;
137 string res;
138 foreach (const Tag t; children) res ~= t.textContent;
139 return res;
142 string toStringNice (int indent=0) const {
143 string res;
144 void addIndent () { foreach (immutable _; 0..indent) res ~= ' '; }
145 void newLine () { res ~= '\n'; foreach (immutable _; 0..indent) res ~= ' '; }
146 if (name.length == 0) return text;
147 res ~= '<';
148 if (children.length == 0) res ~= '/';
149 res ~= name;
150 if (id.length) { res ~= " id="; res ~= id; }
151 if (href.length) { res ~= " href="; res ~= href; }
152 res ~= '>';
153 if (children.length) {
154 if (indent >= 0) indent += 2;
155 foreach (const Tag cc; children) {
156 if (indent >= 0) newLine();
157 res ~= cc.toStringNice(indent);
159 if (indent >= 0) indent -= 2;
160 if (indent >= 0) newLine();
161 res ~= "</";
162 res ~= name;
163 res ~= ">";
165 return res;
168 override string toString () const { return toStringNice(int.min); }
172 // ////////////////////////////////////////////////////////////////////////// //
173 final class BookText {
174 string authorFirst;
175 string authorLast;
176 string title;
177 string sequence;
178 uint seqnum;
180 static struct Image {
181 string id;
182 TrueColorImage img;
183 int nvgid = -1;
186 Tag content; // body
187 Image[] images;
189 this (const(char)[] fname) { loadFile(fname); }
190 this (VFile fl) { loadFile(fl); }
192 private:
193 void loadFile (VFile fl) {
194 auto sax = new SaxyEx();
196 string norm (const(char)[] s) {
197 string res;
198 foreach (char ch; s) {
199 if (ch <= ' ' || ch == 127) {
200 if (res.length && res[$-1] > ' ') res ~= ch;
201 } else {
202 res ~= ch;
205 return res;
208 // sequence tag
209 sax.onOpen("/FictionBook/description/title-info/sequence", (char[] text, char[][string] attrs) {
210 if (auto sn = "name" in attrs) {
211 sequence = (*sn).dup;
212 if (auto id = "number" in attrs) {
213 import std.conv : to;
214 seqnum = to!uint(*id);
215 if (seqnum < 1 || seqnum > 8192) sequence = null;
217 } else {
218 sequence = null;
220 if (sequence.length == 0) { sequence = null; seqnum = 0; }
223 // author's first name
224 sax.onContent("/FictionBook/description/title-info/author/first-name", (char[] text) {
225 authorFirst = norm(text);
227 // author's last name
228 sax.onContent("/FictionBook/description/title-info/author/last-name", (char[] text) {
229 authorLast = norm(text);
231 // book title
232 sax.onContent("/FictionBook/description/title-info/book-title", (char[] text) {
233 title = norm(text);
236 string imageid;
237 string imagefmt;
238 char[] imagectx;
240 sax.onOpen("/FictionBook/binary", (char[] text, char[][string] attrs) {
241 import iv.vfs.io;
242 if (auto ctp = "content-type" in attrs) {
243 if (*ctp == "image/png" || *ctp == "image/jpeg") {
244 if (auto idp = "id" in attrs) {
245 imageid = (*idp).idup;
246 if (imageid.length == 0) {
247 conwriteln("image without id");
248 } else {
249 imagefmt = (*ctp).idup;
252 } else {
253 conwriteln("unknown binary content format: '", *ctp, "'");
257 sax.onContent("/FictionBook/binary", (char[] text) {
258 if (imageid.length) {
259 foreach (char ch; text) if (ch > ' ') imagectx ~= ch;
262 sax.onClose("/FictionBook/binary", (char[] text) {
263 import iv.vfs.io;
264 if (imageid.length) {
265 //conwriteln("image with id '", imageid, "'...");
266 import std.base64;
267 if (imagectx.length == 0) {
268 conwriteln("image with id '", imageid, "' has no data");
269 } else {
270 try {
271 auto imgdata = Base64.decode(imagectx);
272 TrueColorImage img;
273 if (imagefmt == "image/jpeg") {
274 img = readJpegFromMemory(imgdata[]).getAsTrueColorImage;
275 } else if (imagefmt == "image/png") {
276 img = imageFromPng(readPng(imgdata[])).getAsTrueColorImage;
277 } else {
278 assert(0, "wtf?!");
280 if (img.width > 1 && img.height > 1) {
281 if (img.width > 1024 || img.height > 1024) {
282 // scale image
283 float scl = 1024.0f/(img.width > img.height ? img.width : img.height);
284 int nw = cast(int)(img.width*scl);
285 int nh = cast(int)(img.height*scl);
286 if (nw < 1) nw = 1;
287 if (nh < 1) nh = 1;
288 img = imageResize!3(img, nw, nh);
291 images ~= Image(imageid, img);
292 //conwritePng("z_"~imageid~".png", images[$-1].img);
293 } catch (Exception e) {
294 conwriteln("image with id '", imageid, "' has invalid data");
295 conwriteln("ERROR: ", e.msg);
299 imageid = null;
300 imagectx = null;
301 imagefmt = null;
304 Tag[] tagStack;
306 sax.onOpen("/FictionBook/body", (char[] text) {
307 if (content is null) {
308 content = new Tag();
309 content.name = "body";
311 tagStack ~= content;
314 sax.onClose("/FictionBook/body", (char[] text) {
315 if (content is null) assert(0, "wtf?!");
316 tagStack.length -= 1;
317 tagStack.assumeSafeAppend;
320 sax.onOpen("/FictionBook/body/+", (char[] text, char[][string] attrs) {
321 auto tag = new Tag();
322 tag.name = text.idup;
323 tag.parent = tagStack[$-1];
325 if (auto ls = tag.parent.lastChild) {
326 ls.nextSibling = tag;
327 tag.prevSibling = ls;
330 tag.parent.children ~= tag;
331 if (auto p = "id" in attrs) tag.id = norm(*p);
332 if (auto p = "href" in attrs) tag.href = norm(*p);
333 else if (auto p = "l:href" in attrs) tag.href = norm(*p);
335 if (tag.name == "image") {
336 import iv.vfs.io;
337 conwriteln("IMAGE: ", tag.href);
340 tagStack ~= tag;
343 sax.onClose("/FictionBook/body/+", (char[] text) {
344 tagStack.length -= 1;
345 tagStack.assumeSafeAppend;
348 sax.onContent("/FictionBook/body/+", (char[] text) {
349 auto tag = new Tag();
350 tag.name = null;
351 tag.parent = tagStack[$-1];
353 if (auto ls = tag.parent.lastChild) {
354 ls.nextSibling = tag;
355 tag.prevSibling = ls;
358 tag.parent.children ~= tag;
359 tag.text ~= text.idup;
362 sax.loadStream(fl);
364 if (content is null) {
365 content = new Tag();
366 content.name = "body";
370 void loadFile (const(char)[] fname) {
371 import std.path : extension;
372 if (strEquCI(fname.extension, ".zip")) {
373 auto did = vfsAddPak!"first"(fname);
374 scope(exit) vfsRemovePak(did);
375 foreach (immutable idx, ref de; vfsFileList) {
376 if (strEquCI(de.name.extension, ".fb2")) { loadFile(vfsOpenFile(de.name)); return; }
378 throw new Exception("no fb2 file found in '"~fname.idup~"'");
379 } else {
380 loadFile(vfsOpenFile(fname));