it is now possible to remove file from recent list; xreader will automatically clean...
[xreader.git] / booktext.d
blob3f385693d1a7e916254b430f99c09994307dcabc
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.nanovg;
28 import iv.saxy;
29 import iv.strex;
30 import iv.utfutil;
31 import iv.vfs;
33 import xlayouter;
36 // ////////////////////////////////////////////////////////////////////////// //
37 struct BookInfo {
38 string author;
39 string title;
40 string seqname;
41 uint seqnum;
42 string diskfile; // not set by `loadBookInfo()`!
46 BookInfo loadBookInfo (const(char)[] fname) {
47 static BookInfo loadFile (VFile fl) {
48 BookInfo res;
49 auto sax = new SaxyEx();
50 bool complete = false;
51 string authorFirst, authorLast;
53 inout(char)[] strip (inout(char)[] s) inout {
54 while (s.length && s.ptr[0] <= ' ') s = s[1..$];
55 while (s.length && s[$-1] <= ' ') s = s[0..$-1];
56 return s;
59 try {
60 // sequence tag
61 sax.onOpen("/FictionBook/description/title-info/sequence", (char[] text, char[][string] attrs) {
62 if (auto sn = "name" in attrs) {
63 res.seqname = strip(*sn).dup;
64 if (auto id = "number" in attrs) {
65 import std.conv : to;
66 res.seqnum = to!uint(*id);
67 if (res.seqnum < 1 || res.seqnum > 8192) res.seqname = null;
69 } else {
70 res.seqname = null;
72 if (res.seqname.length == 0) { res.seqname = null; res.seqnum = 0; }
73 });
75 sax.onClose("/FictionBook/description", (char[] text) {
76 complete = true;
77 throw new Exception("done"); // sorry
78 });
80 // author's first name
81 sax.onContent("/FictionBook/description/title-info/author/first-name", (char[] text) {
82 authorFirst = strip(text).idup;
83 });
84 // author's last name
85 sax.onContent("/FictionBook/description/title-info/author/last-name", (char[] text) {
86 authorLast = strip(text).idup;
87 });
88 // book title
89 sax.onContent("/FictionBook/description/title-info/book-title", (char[] text) {
90 res.title = strip(text).idup;
91 });
93 sax.loadStream(fl);
94 } catch (Exception e) {
95 if (!complete) throw e;
97 if (authorFirst.length == 0) {
98 authorFirst = authorLast;
99 authorLast = null;
100 } else if (authorLast.length != 0) {
101 authorFirst ~= " "~authorLast;
103 if (authorFirst.length == 0 && res.title.length == 0) throw new Exception("no book title found");
104 res.author = authorFirst;
105 return res;
108 import std.path : extension;
109 if (strEquCI(fname.extension, ".zip")) {
110 auto did = vfsAddPak!"first"(fname);
111 scope(exit) vfsRemovePak(did);
112 foreach (immutable idx, ref de; vfsFileList) {
113 if (strEquCI(de.name.extension, ".fb2")) return loadFile(vfsOpenFile(de.name));
115 throw new Exception("no fb2 file found in '"~fname.idup~"'");
116 } else {
117 return loadFile(vfsOpenFile(fname));
122 // ////////////////////////////////////////////////////////////////////////// //
123 final class Tag {
124 string name; // empty: text node
125 string id;
126 string href;
127 string text;
128 Tag[] children;
129 Tag parent;
130 //Tag prevSibling;
131 //Tag nextSibling;
133 @property inout(Tag) firstChild () inout pure nothrow @trusted @nogc { pragma(inline, true); return (children.length ? children.ptr[0] : null); }
134 @property inout(Tag) lastChild () inout pure nothrow @trusted @nogc { pragma(inline, true); return (children.length ? children[$-1] : null); }
136 string textContent () const {
137 if (name.length == 0) return text;
138 string res;
139 foreach (const Tag t; children) res ~= t.textContent;
140 return res;
143 string toStringNice (int indent=0) const {
144 string res;
145 void addIndent () { foreach (immutable _; 0..indent) res ~= ' '; }
146 void newLine () { res ~= '\n'; foreach (immutable _; 0..indent) res ~= ' '; }
147 if (name.length == 0) return text;
148 res ~= '<';
149 if (children.length == 0) res ~= '/';
150 res ~= name;
151 if (id.length) { res ~= " id="; res ~= id; }
152 if (href.length) { res ~= " href="; res ~= href; }
153 res ~= '>';
154 if (children.length) {
155 if (indent >= 0) indent += 2;
156 foreach (const Tag cc; children) {
157 if (indent >= 0) newLine();
158 res ~= cc.toStringNice(indent);
160 if (indent >= 0) indent -= 2;
161 if (indent >= 0) newLine();
162 res ~= "</";
163 res ~= name;
164 res ~= ">";
166 return res;
169 override string toString () const { return toStringNice(int.min); }
173 // ////////////////////////////////////////////////////////////////////////// //
174 final class BookText {
175 string authorFirst;
176 string authorLast;
177 string title;
178 string sequence;
179 uint seqnum;
181 static struct Image {
182 string id;
183 TrueColorImage img;
184 int nvgid = -1;
187 Tag content; // body
188 Image[] images;
190 this (const(char)[] fname) { loadFile(fname); }
191 this (VFile fl) { loadFile(fl); }
193 private:
194 void loadFile (VFile fl) {
195 auto sax = new SaxyEx();
197 string norm (const(char)[] s) {
198 string res;
199 foreach (char ch; s) {
200 if (ch <= ' ' || ch == 127) {
201 if (res.length && res[$-1] > ' ') res ~= ch;
202 } else {
203 res ~= ch;
206 return res;
209 // sequence tag
210 sax.onOpen("/FictionBook/description/title-info/sequence", (char[] text, char[][string] attrs) {
211 if (auto sn = "name" in attrs) {
212 sequence = (*sn).dup;
213 if (auto id = "number" in attrs) {
214 import std.conv : to;
215 seqnum = to!uint(*id);
216 if (seqnum < 1 || seqnum > 8192) sequence = null;
218 } else {
219 sequence = null;
221 if (sequence.length == 0) { sequence = null; seqnum = 0; }
224 // author's first name
225 sax.onContent("/FictionBook/description/title-info/author/first-name", (char[] text) {
226 authorFirst = norm(text);
228 // author's last name
229 sax.onContent("/FictionBook/description/title-info/author/last-name", (char[] text) {
230 authorLast = norm(text);
232 // book title
233 sax.onContent("/FictionBook/description/title-info/book-title", (char[] text) {
234 title = norm(text);
237 string imageid;
238 string imagefmt;
239 char[] imagectx;
241 sax.onOpen("/FictionBook/binary", (char[] text, char[][string] attrs) {
242 import iv.vfs.io;
243 if (auto ctp = "content-type" in attrs) {
244 if (*ctp == "image/png" || *ctp == "image/jpeg") {
245 if (auto idp = "id" in attrs) {
246 imageid = (*idp).idup;
247 if (imageid.length == 0) {
248 conwriteln("image without id");
249 } else {
250 imagefmt = (*ctp).idup;
253 } else {
254 conwriteln("unknown binary content format: '", *ctp, "'");
258 sax.onContent("/FictionBook/binary", (char[] text) {
259 if (imageid.length) {
260 foreach (char ch; text) if (ch > ' ') imagectx ~= ch;
263 sax.onClose("/FictionBook/binary", (char[] text) {
264 import iv.vfs.io;
265 if (imageid.length) {
266 //conwriteln("image with id '", imageid, "'...");
267 //import std.base64;
268 if (imagectx.length == 0) {
269 conwriteln("image with id '", imageid, "' has no data");
270 } else {
271 try {
272 //auto imgdata = Base64.decode(imagectx);
273 auto imgdata = base64Decode(imagectx);
274 TrueColorImage img;
275 MemoryImage memimg;
276 if (imagefmt == "image/jpeg") {
277 memimg = readJpegFromMemory(imgdata[]);
278 } else if (imagefmt == "image/png") {
279 memimg = imageFromPng(readPng(imgdata[]));
280 } else {
281 assert(0, "wtf?!");
283 if (memimg is null) {
284 conwriteln("fucked image, id '", imageid, "'");
285 } else {
286 img = cast(TrueColorImage)memimg;
287 if (img is null) {
288 img = memimg.getAsTrueColorImage;
289 delete memimg;
291 if (img.width > 1 && img.height > 1) {
292 if (img.width > 1024 || img.height > 1024) {
293 // scale image
294 float scl = 1024.0f/(img.width > img.height ? img.width : img.height);
295 int nw = cast(int)(img.width*scl);
296 int nh = cast(int)(img.height*scl);
297 if (nw < 1) nw = 1;
298 if (nh < 1) nh = 1;
299 img = imageResize!3(img, nw, nh);
302 images ~= Image(imageid, img);
303 //conwritePng("z_"~imageid~".png", images[$-1].img);
305 } catch (Exception e) {
306 conwriteln("image with id '", imageid, "' has invalid data");
307 conwriteln("ERROR: ", e.msg);
311 imageid = null;
312 imagectx = null;
313 imagefmt = null;
316 Tag[] tagStack;
318 sax.onOpen("/FictionBook/body", (char[] text) {
319 if (content is null) {
320 content = new Tag();
321 content.name = "body";
323 tagStack ~= content;
326 sax.onClose("/FictionBook/body", (char[] text) {
327 if (content is null) assert(0, "wtf?!");
328 tagStack.length -= 1;
329 tagStack.assumeSafeAppend;
332 sax.onOpen("/FictionBook/body/+", (char[] text, char[][string] attrs) {
333 auto tag = new Tag();
334 tag.name = text.idup;
335 tag.parent = tagStack[$-1];
337 if (auto ls = tag.parent.lastChild) {
338 ls.nextSibling = tag;
339 tag.prevSibling = ls;
342 tag.parent.children ~= tag;
343 if (auto p = "id" in attrs) tag.id = norm(*p);
344 if (auto p = "href" in attrs) tag.href = norm(*p);
345 else if (auto p = "l:href" in attrs) tag.href = norm(*p);
347 if (tag.name == "image") {
348 import iv.vfs.io;
349 conwriteln("IMAGE: ", tag.href);
352 tagStack ~= tag;
355 sax.onClose("/FictionBook/body/+", (char[] text) {
356 tagStack.length -= 1;
357 tagStack.assumeSafeAppend;
360 sax.onContent("/FictionBook/body/+", (char[] text) {
361 auto tag = new Tag();
362 tag.name = null;
363 tag.parent = tagStack[$-1];
365 if (auto ls = tag.parent.lastChild) {
366 ls.nextSibling = tag;
367 tag.prevSibling = ls;
370 tag.parent.children ~= tag;
371 tag.text ~= text.idup;
374 sax.loadStream(fl);
376 if (content is null) {
377 content = new Tag();
378 content.name = "body";
382 void loadFile (const(char)[] fname) {
383 import std.path : extension;
384 if (strEquCI(fname.extension, ".zip")) {
385 auto did = vfsAddPak!"first"(fname);
386 scope(exit) vfsRemovePak(did);
387 foreach (immutable idx, ref de; vfsFileList) {
388 if (strEquCI(de.name.extension, ".fb2")) { loadFile(vfsOpenFile(de.name)); return; }
390 throw new Exception("no fb2 file found in '"~fname.idup~"'");
391 } else {
392 loadFile(vfsOpenFile(fname));