(experimental) it is now possible to select navigation link with keyboard
[xreader.git] / booktext.d
blob2876324991b1f9626934350a07157c2752fd3bf5
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.base64;
24 import iv.cmdcon;
25 import iv.encoding;
26 //import iv.iresample;
27 import iv.nanovega;
28 import iv.nanovega.textlayouter;
29 import iv.saxy;
30 import iv.strex;
31 import iv.utfutil;
32 import iv.vfs;
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" || *ctp == "image/jpg") {
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 auto imgdata = base64Decode(imagectx);
273 TrueColorImage img;
274 MemoryImage memimg;
275 if (imagefmt == "image/jpeg" || imagefmt == "image/jpg") {
276 memimg = readJpegFromMemory(imgdata[]);
277 } else if (imagefmt == "image/png") {
278 memimg = imageFromPng(readPng(imgdata[]));
279 } else {
280 assert(0, "wtf?!");
282 if (memimg is null) {
283 conwriteln("fucked image, id '", imageid, "'");
284 } else {
285 img = cast(TrueColorImage)memimg;
286 if (img is null) {
287 img = memimg.getAsTrueColorImage;
288 delete memimg;
290 if (img.width > 1 && img.height > 1) {
291 if (img.width > 1024 || img.height > 1024) {
292 // scale image
293 float scl = 1024.0f/(img.width > img.height ? img.width : img.height);
294 int nw = cast(int)(img.width*scl);
295 int nh = cast(int)(img.height*scl);
296 if (nw < 1) nw = 1;
297 if (nh < 1) nh = 1;
298 img = imageResize!3(img, nw, nh);
301 images ~= Image(imageid, img);
302 //conwritePng("z_"~imageid~".png", images[$-1].img);
304 } catch (Exception e) {
305 conwriteln("image with id '", imageid, "' has invalid data");
306 conwriteln("ERROR: ", e.msg);
310 imageid = null;
311 imagectx = null;
312 imagefmt = null;
315 Tag[] tagStack;
317 sax.onOpen("/FictionBook/body", (char[] text) {
318 if (content is null) {
319 content = new Tag();
320 content.name = "body";
322 tagStack ~= content;
325 sax.onClose("/FictionBook/body", (char[] text) {
326 if (content is null) assert(0, "wtf?!");
327 tagStack.length -= 1;
328 tagStack.assumeSafeAppend;
331 sax.onOpen("/FictionBook/body/+", (char[] text, char[][string] attrs) {
332 auto tag = new Tag();
333 tag.name = text.idup;
334 tag.parent = tagStack[$-1];
336 if (auto ls = tag.parent.lastChild) {
337 ls.nextSibling = tag;
338 tag.prevSibling = ls;
341 tag.parent.children ~= tag;
342 if (auto p = "id" in attrs) tag.id = norm(*p);
343 if (auto p = "href" in attrs) tag.href = norm(*p);
344 else if (auto p = "l:href" in attrs) tag.href = norm(*p);
346 if (tag.name == "image") {
347 import iv.vfs.io;
348 conwriteln("IMAGE: ", tag.href);
351 tagStack ~= tag;
354 sax.onClose("/FictionBook/body/+", (char[] text) {
355 tagStack.length -= 1;
356 tagStack.assumeSafeAppend;
359 sax.onContent("/FictionBook/body/+", (char[] text) {
360 auto tag = new Tag();
361 tag.name = null;
362 tag.parent = tagStack[$-1];
364 if (auto ls = tag.parent.lastChild) {
365 ls.nextSibling = tag;
366 tag.prevSibling = ls;
369 tag.parent.children ~= tag;
370 tag.text ~= text.idup;
373 sax.loadStream(fl);
375 if (content is null) {
376 content = new Tag();
377 content.name = "body";
381 void loadFile (const(char)[] fname) {
382 import std.path : extension;
383 if (strEquCI(fname.extension, ".zip")) {
384 auto did = vfsAddPak!"first"(fname);
385 scope(exit) vfsRemovePak(did);
386 foreach (immutable idx, ref de; vfsFileList) {
387 if (strEquCI(de.name.extension, ".fb2")) { loadFile(vfsOpenFile(de.name)); return; }
389 throw new Exception("no fb2 file found in '"~fname.idup~"'");
390 } else {
391 loadFile(vfsOpenFile(fname));