redraw each frame, or driver (system?) will decrease our priority, and we won't get...
[xreader.git] / booktext.d
blobf96641be6aafd8abbab2ba0a0c08af8eda325e54
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.encoding;
24 import iv.iresample;
25 import iv.nanovg;
26 import iv.saxy;
27 import iv.strex;
28 import iv.utfutil;
29 import iv.vfs;
31 import xlayouter;
34 // ////////////////////////////////////////////////////////////////////////// //
35 struct BookInfo {
36 string author;
37 string title;
38 string seqname;
39 uint seqnum;
40 string diskfile; // not set by `loadBookInfo()`!
44 BookInfo loadBookInfo (const(char)[] fname) {
45 static BookInfo loadFile (VFile fl) {
46 BookInfo res;
47 auto sax = new SaxyEx();
48 bool complete = false;
49 string authorFirst, authorLast;
51 inout(char)[] strip (inout(char)[] s) inout {
52 while (s.length && s.ptr[0] <= ' ') s = s[1..$];
53 while (s.length && s[$-1] <= ' ') s = s[0..$-1];
54 return s;
57 try {
58 // sequence tag
59 sax.onOpen("/FictionBook/description/title-info/sequence", (char[] text, char[][string] attrs) {
60 if (auto sn = "name" in attrs) {
61 res.seqname = strip(*sn).dup;
62 if (auto id = "number" in attrs) {
63 import std.conv : to;
64 res.seqnum = to!uint(*id);
65 if (res.seqnum < 1 || res.seqnum > 8192) res.seqname = null;
67 } else {
68 res.seqname = null;
70 if (res.seqname.length == 0) { res.seqname = null; res.seqnum = 0; }
71 });
73 sax.onClose("/FictionBook/description", (char[] text) {
74 complete = true;
75 throw new Exception("done"); // sorry
76 });
78 // author's first name
79 sax.onContent("/FictionBook/description/title-info/author/first-name", (char[] text) {
80 authorFirst = strip(text).idup;
81 });
82 // author's last name
83 sax.onContent("/FictionBook/description/title-info/author/last-name", (char[] text) {
84 authorLast = strip(text).idup;
85 });
86 // book title
87 sax.onContent("/FictionBook/description/title-info/book-title", (char[] text) {
88 res.title = strip(text).idup;
89 });
91 sax.loadStream(fl);
92 } catch (Exception e) {
93 if (!complete) throw e;
95 if (authorFirst.length == 0) {
96 authorFirst = authorLast;
97 authorLast = null;
98 } else if (authorLast.length != 0) {
99 authorFirst ~= " "~authorLast;
101 if (authorFirst.length == 0 && res.title.length == 0) throw new Exception("no book title found");
102 res.author = authorFirst;
103 return res;
106 import std.path : extension;
107 if (strEquCI(fname.extension, ".zip")) {
108 auto did = vfsAddPak!"first"(fname);
109 scope(exit) vfsRemovePack(did);
110 foreach (immutable idx, ref de; vfsFileList) {
111 if (strEquCI(de.name.extension, ".fb2")) return loadFile(vfsOpenFile(de.name));
113 throw new Exception("no fb2 file found in '"~fname.idup~"'");
114 } else {
115 return loadFile(vfsOpenFile(fname));
120 // ////////////////////////////////////////////////////////////////////////// //
121 final class Tag {
122 string name; // empty: text node
123 string id;
124 string href;
125 string text;
126 Tag[] children;
127 Tag parent;
128 //Tag prevSibling;
129 //Tag nextSibling;
131 @property inout(Tag) firstChild () inout pure nothrow @trusted @nogc { pragma(inline, true); return (children.length ? children.ptr[0] : null); }
132 @property inout(Tag) lastChild () inout pure nothrow @trusted @nogc { pragma(inline, true); return (children.length ? children[$-1] : null); }
134 string textContent () const {
135 if (name.length == 0) return text;
136 string res;
137 foreach (const Tag t; children) res ~= t.textContent;
138 return res;
141 string toStringNice (int indent=0) const {
142 string res;
143 void addIndent () { foreach (immutable _; 0..indent) res ~= ' '; }
144 void newLine () { res ~= '\n'; foreach (immutable _; 0..indent) res ~= ' '; }
145 if (name.length == 0) return text;
146 res ~= '<';
147 if (children.length == 0) res ~= '/';
148 res ~= name;
149 if (id.length) { res ~= " id="; res ~= id; }
150 if (href.length) { res ~= " href="; res ~= href; }
151 res ~= '>';
152 if (children.length) {
153 if (indent >= 0) indent += 2;
154 foreach (const Tag cc; children) {
155 if (indent >= 0) newLine();
156 res ~= cc.toStringNice(indent);
158 if (indent >= 0) indent -= 2;
159 if (indent >= 0) newLine();
160 res ~= "</";
161 res ~= name;
162 res ~= ">";
164 return res;
167 override string toString () const { return toStringNice(int.min); }
171 // ////////////////////////////////////////////////////////////////////////// //
172 final class BookText {
173 string authorFirst;
174 string authorLast;
175 string title;
176 string sequence;
177 uint seqnum;
179 static struct Image {
180 string id;
181 TrueColorImage img;
182 int nvgid = -1;
185 Tag content; // body
186 Image[] images;
188 this (const(char)[] fname) { loadFile(fname); }
189 this (VFile fl) { loadFile(fl); }
191 private:
192 void loadFile (VFile fl) {
193 auto sax = new SaxyEx();
195 string norm (const(char)[] s) {
196 string res;
197 foreach (char ch; s) {
198 if (ch <= ' ' || ch == 127) {
199 if (res.length && res[$-1] > ' ') res ~= ch;
200 } else {
201 res ~= ch;
204 return res;
207 // sequence tag
208 sax.onOpen("/FictionBook/description/title-info/sequence", (char[] text, char[][string] attrs) {
209 if (auto sn = "name" in attrs) {
210 sequence = (*sn).dup;
211 if (auto id = "number" in attrs) {
212 import std.conv : to;
213 seqnum = to!uint(*id);
214 if (seqnum < 1 || seqnum > 8192) sequence = null;
216 } else {
217 sequence = null;
219 if (sequence.length == 0) { sequence = null; seqnum = 0; }
222 // author's first name
223 sax.onContent("/FictionBook/description/title-info/author/first-name", (char[] text) {
224 authorFirst = norm(text);
226 // author's last name
227 sax.onContent("/FictionBook/description/title-info/author/last-name", (char[] text) {
228 authorLast = norm(text);
230 // book title
231 sax.onContent("/FictionBook/description/title-info/book-title", (char[] text) {
232 title = norm(text);
235 string imageid;
236 string imagefmt;
237 char[] imagectx;
239 sax.onOpen("/FictionBook/binary", (char[] text, char[][string] attrs) {
240 import iv.vfs.io;
241 if (auto ctp = "content-type" in attrs) {
242 if (*ctp == "image/png" || *ctp == "image/jpeg") {
243 if (auto idp = "id" in attrs) {
244 imageid = (*idp).idup;
245 if (imageid.length == 0) {
246 writeln("image without id");
247 } else {
248 imagefmt = (*ctp).idup;
251 } else {
252 writeln("unknown binary content format: '", *ctp, "'");
256 sax.onContent("/FictionBook/binary", (char[] text) {
257 if (imageid.length) {
258 foreach (char ch; text) if (ch > ' ') imagectx ~= ch;
261 sax.onClose("/FictionBook/binary", (char[] text) {
262 import iv.vfs.io;
263 if (imageid.length) {
264 //writeln("image with id '", imageid, "'...");
265 import std.base64;
266 if (imagectx.length == 0) {
267 writeln("image with id '", imageid, "' has no data");
268 } else {
269 try {
270 auto imgdata = Base64.decode(imagectx);
271 TrueColorImage img;
272 if (imagefmt == "image/jpeg") {
273 img = readJpegFromMemory(imgdata[]).getAsTrueColorImage;
274 } else if (imagefmt == "image/png") {
275 img = imageFromPng(readPng(imgdata[])).getAsTrueColorImage;
276 } else {
277 assert(0, "wtf?!");
279 if (img.width > 1 && img.height > 1) {
280 if (img.width > 1024 || img.height > 1024) {
281 // scale image
282 float scl = 1024.0f/(img.width > img.height ? img.width : img.height);
283 int nw = cast(int)(img.width*scl);
284 int nh = cast(int)(img.height*scl);
285 if (nw < 1) nw = 1;
286 if (nh < 1) nh = 1;
287 img = imageResample(img, nw, nh, RESAMPLER_DEFAULT_FILTER);
290 images ~= Image(imageid, img);
291 //writePng("z_"~imageid~".png", images[$-1].img);
292 } catch (Exception e) {
293 writeln("image with id '", imageid, "' has invalid data");
294 writeln("ERROR: ", e.msg);
298 imageid = null;
299 imagectx = null;
300 imagefmt = null;
303 Tag[] tagStack;
305 sax.onOpen("/FictionBook/body", (char[] text) {
306 if (content is null) {
307 content = new Tag();
308 content.name = "body";
310 tagStack ~= content;
313 sax.onClose("/FictionBook/body", (char[] text) {
314 if (content is null) assert(0, "wtf?!");
315 tagStack.length -= 1;
316 tagStack.assumeSafeAppend;
319 sax.onOpen("/FictionBook/body/+", (char[] text, char[][string] attrs) {
320 auto tag = new Tag();
321 tag.name = text.idup;
322 tag.parent = tagStack[$-1];
324 if (auto ls = tag.parent.lastChild) {
325 ls.nextSibling = tag;
326 tag.prevSibling = ls;
329 tag.parent.children ~= tag;
330 if (auto p = "id" in attrs) tag.id = norm(*p);
331 if (auto p = "href" in attrs) tag.href = norm(*p);
332 else if (auto p = "l:href" in attrs) tag.href = norm(*p);
334 if (tag.name == "image") {
335 import iv.vfs.io;
336 writeln("IMAGE: ", tag.href);
339 tagStack ~= tag;
342 sax.onClose("/FictionBook/body/+", (char[] text) {
343 tagStack.length -= 1;
344 tagStack.assumeSafeAppend;
347 sax.onContent("/FictionBook/body/+", (char[] text) {
348 auto tag = new Tag();
349 tag.name = null;
350 tag.parent = tagStack[$-1];
352 if (auto ls = tag.parent.lastChild) {
353 ls.nextSibling = tag;
354 tag.prevSibling = ls;
357 tag.parent.children ~= tag;
358 tag.text ~= text.idup;
361 sax.loadStream(fl);
363 if (content is null) {
364 content = new Tag();
365 content.name = "body";
369 void loadFile (const(char)[] fname) {
370 import std.path : extension;
371 if (strEquCI(fname.extension, ".zip")) {
372 auto did = vfsAddPak!"first"(fname);
373 scope(exit) vfsRemovePack(did);
374 foreach (immutable idx, ref de; vfsFileList) {
375 if (strEquCI(de.name.extension, ".fb2")) { loadFile(vfsOpenFile(de.name)); return; }
377 throw new Exception("no fb2 file found in '"~fname.idup~"'");
378 } else {
379 loadFile(vfsOpenFile(fname));