do not change page size
[xreader.git] / booktext.d
blobd1eff09db30cb4c49e4013b26dea940042546c72
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()`!
42 // for db
43 int id;
44 bool needupdate;
48 BookInfo loadBookInfo (const(char)[] fname) {
49 static BookInfo loadFile (VFile fl) {
50 BookInfo res;
51 auto sax = new SaxyEx();
52 bool complete = false;
53 string authorFirst, authorLast;
55 inout(char)[] strip (inout(char)[] s) inout {
56 while (s.length && s.ptr[0] <= ' ') s = s[1..$];
57 while (s.length && s[$-1] <= ' ') s = s[0..$-1];
58 return s;
61 try {
62 // sequence tag
63 sax.onOpen("/FictionBook/description/title-info/sequence", (char[] text, char[][string] attrs) {
64 if (auto sn = "name" in attrs) {
65 res.seqname = strip(*sn).dup;
66 if (auto id = "number" in attrs) {
67 import std.conv : to;
68 res.seqnum = to!uint(*id);
69 if (res.seqnum < 1 || res.seqnum > 8192) res.seqname = null;
71 } else {
72 res.seqname = null;
74 if (res.seqname.length == 0) { res.seqname = null; res.seqnum = 0; }
75 });
77 sax.onClose("/FictionBook/description", (char[] text) {
78 complete = true;
79 throw new Exception("done"); // sorry
80 });
82 // author's first name
83 sax.onContent("/FictionBook/description/title-info/author/first-name", (char[] text) {
84 authorFirst = strip(text).idup;
85 });
86 // author's last name
87 sax.onContent("/FictionBook/description/title-info/author/last-name", (char[] text) {
88 authorLast = strip(text).idup;
89 });
90 // book title
91 sax.onContent("/FictionBook/description/title-info/book-title", (char[] text) {
92 res.title = strip(text).idup;
93 });
95 sax.loadStream(fl);
96 } catch (Exception e) {
97 if (!complete) throw e;
99 if (authorFirst.length == 0) {
100 authorFirst = authorLast;
101 authorLast = null;
102 } else if (authorLast.length != 0) {
103 authorFirst ~= " "~authorLast;
105 if (authorFirst.length == 0 && res.title.length == 0) throw new Exception("no book title found");
106 res.author = authorFirst;
107 return res;
110 import std.path : extension;
111 if (strEquCI(fname.extension, ".zip")) {
112 auto did = vfsAddPak!"first"(fname);
113 scope(exit) vfsRemovePak(did);
114 foreach (immutable idx, ref de; vfsFileList) {
115 if (strEquCI(de.name.extension, ".fb2")) return loadFile(vfsOpenFile(de.name));
117 throw new Exception("no fb2 file found in '"~fname.idup~"'");
118 } else {
119 return loadFile(vfsOpenFile(fname));
124 // ////////////////////////////////////////////////////////////////////////// //
125 final class Tag {
126 string name; // empty: text node
127 string id;
128 string href;
129 string text;
130 Tag[] children;
131 Tag parent;
132 //Tag prevSibling;
133 //Tag nextSibling;
135 @property inout(Tag) firstChild () inout pure nothrow @trusted @nogc { pragma(inline, true); return (children.length ? children.ptr[0] : null); }
136 @property inout(Tag) lastChild () inout pure nothrow @trusted @nogc { pragma(inline, true); return (children.length ? children[$-1] : null); }
138 string textContent () const {
139 if (name.length == 0) return text;
140 string res;
141 foreach (const Tag t; children) res ~= t.textContent;
142 return res;
145 string toStringNice (int indent=0) const {
146 string res;
147 void addIndent () { foreach (immutable _; 0..indent) res ~= ' '; }
148 void newLine () { res ~= '\n'; foreach (immutable _; 0..indent) res ~= ' '; }
149 if (name.length == 0) return text;
150 res ~= '<';
151 if (children.length == 0) res ~= '/';
152 res ~= name;
153 if (id.length) { res ~= " id="; res ~= id; }
154 if (href.length) { res ~= " href="; res ~= href; }
155 res ~= '>';
156 if (children.length) {
157 if (indent >= 0) indent += 2;
158 foreach (const Tag cc; children) {
159 if (indent >= 0) newLine();
160 res ~= cc.toStringNice(indent);
162 if (indent >= 0) indent -= 2;
163 if (indent >= 0) newLine();
164 res ~= "</";
165 res ~= name;
166 res ~= ">";
168 return res;
171 override string toString () const { return toStringNice(int.min); }
175 // ////////////////////////////////////////////////////////////////////////// //
176 final class BookText {
177 string authorFirst;
178 string authorLast;
179 string title;
180 string sequence;
181 uint seqnum;
183 static struct Image {
184 string id;
185 TrueColorImage img;
186 int nvgid = -1;
189 Tag content; // body
190 Image[] images;
192 this (const(char)[] fname) { loadFile(fname); }
193 this (VFile fl) { loadFile(fl); }
195 string getAuthor () {
196 if (authorLast.length != 0) return authorFirst~" "~authorLast;
197 return authorFirst;
200 string getTitle () => title;
202 private:
203 void loadFile (VFile fl) {
204 auto sax = new SaxyEx();
206 string norm (const(char)[] s) {
207 string res;
208 foreach (char ch; s) {
209 if (ch <= ' ' || ch == 127) {
210 if (res.length && res[$-1] > ' ') res ~= ch;
211 } else {
212 res ~= ch;
215 return res;
218 // sequence tag
219 sax.onOpen("/FictionBook/description/title-info/sequence", (char[] text, char[][string] attrs) {
220 if (auto sn = "name" in attrs) {
221 sequence = (*sn).dup;
222 if (auto id = "number" in attrs) {
223 import std.conv : to;
224 seqnum = to!uint(*id);
225 if (seqnum < 1 || seqnum > 8192) sequence = null;
227 } else {
228 sequence = null;
230 if (sequence.length == 0) { sequence = null; seqnum = 0; }
233 // author's first name
234 sax.onContent("/FictionBook/description/title-info/author/first-name", (char[] text) {
235 authorFirst = norm(text);
237 // author's last name
238 sax.onContent("/FictionBook/description/title-info/author/last-name", (char[] text) {
239 authorLast = norm(text);
241 // book title
242 sax.onContent("/FictionBook/description/title-info/book-title", (char[] text) {
243 title = norm(text);
246 if (authorFirst.length == 0) {
247 authorFirst = authorLast;
248 authorLast = null;
251 string imageid;
252 string imagefmt;
253 char[] imagectx;
255 sax.onOpen("/FictionBook/binary", (char[] text, char[][string] attrs) {
256 import iv.vfs.io;
257 if (auto ctp = "content-type" in attrs) {
258 if (*ctp == "image/png" || *ctp == "image/jpeg" || *ctp == "image/jpg") {
259 if (auto idp = "id" in attrs) {
260 imageid = (*idp).idup;
261 if (imageid.length == 0) {
262 conwriteln("image without id");
263 } else {
264 imagefmt = (*ctp).idup;
267 } else {
268 conwriteln("unknown binary content format: '", *ctp, "'");
272 sax.onContent("/FictionBook/binary", (char[] text) {
273 if (imageid.length) {
274 foreach (char ch; text) if (ch > ' ') imagectx ~= ch;
277 sax.onClose("/FictionBook/binary", (char[] text) {
278 import iv.vfs.io;
279 if (imageid.length) {
280 //conwriteln("image with id '", imageid, "'...");
281 //import std.base64;
282 if (imagectx.length == 0) {
283 conwriteln("image with id '", imageid, "' has no data");
284 } else {
285 try {
286 //auto imgdata = Base64.decode(imagectx);
287 auto imgdata = base64Decode(imagectx);
288 TrueColorImage img;
289 MemoryImage memimg;
290 if (imagefmt == "image/jpeg" || imagefmt == "image/jpg") {
291 memimg = readJpegFromMemory(imgdata[]);
292 } else if (imagefmt == "image/png") {
293 memimg = imageFromPng(readPng(imgdata[]));
294 } else {
295 assert(0, "wtf?!");
297 if (memimg is null) {
298 conwriteln("fucked image, id '", imageid, "'");
299 } else {
300 img = cast(TrueColorImage)memimg;
301 if (img is null) {
302 img = memimg.getAsTrueColorImage;
303 delete memimg;
305 if (img.width > 1 && img.height > 1) {
306 if (img.width > 1024 || img.height > 1024) {
307 // scale image
308 float scl = 1024.0f/(img.width > img.height ? img.width : img.height);
309 int nw = cast(int)(img.width*scl);
310 int nh = cast(int)(img.height*scl);
311 if (nw < 1) nw = 1;
312 if (nh < 1) nh = 1;
313 img = imageResize!3(img, nw, nh);
316 images ~= Image(imageid, img);
317 //conwritePng("z_"~imageid~".png", images[$-1].img);
319 } catch (Exception e) {
320 conwriteln("image with id '", imageid, "' has invalid data");
321 conwriteln("ERROR: ", e.msg);
325 imageid = null;
326 imagectx = null;
327 imagefmt = null;
330 Tag[] tagStack;
332 sax.onOpen("/FictionBook/body", (char[] text) {
333 if (content is null) {
334 content = new Tag();
335 content.name = "body";
337 tagStack ~= content;
340 sax.onClose("/FictionBook/body", (char[] text) {
341 if (content is null) assert(0, "wtf?!");
342 tagStack.length -= 1;
343 tagStack.assumeSafeAppend;
346 sax.onOpen("/FictionBook/body/+", (char[] text, char[][string] attrs) {
347 auto tag = new Tag();
348 tag.name = text.idup;
349 tag.parent = tagStack[$-1];
351 if (auto ls = tag.parent.lastChild) {
352 ls.nextSibling = tag;
353 tag.prevSibling = ls;
356 tag.parent.children ~= tag;
357 if (auto p = "id" in attrs) tag.id = norm(*p);
358 if (auto p = "href" in attrs) tag.href = norm(*p);
359 else if (auto p = "l:href" in attrs) tag.href = norm(*p);
361 if (tag.name == "image") {
362 import iv.vfs.io;
363 conwriteln("IMAGE: ", tag.href);
366 tagStack ~= tag;
369 sax.onClose("/FictionBook/body/+", (char[] text) {
370 tagStack.length -= 1;
371 tagStack.assumeSafeAppend;
374 sax.onContent("/FictionBook/body/+", (char[] text) {
375 auto tag = new Tag();
376 tag.name = null;
377 tag.parent = tagStack[$-1];
379 if (auto ls = tag.parent.lastChild) {
380 ls.nextSibling = tag;
381 tag.prevSibling = ls;
384 tag.parent.children ~= tag;
385 tag.text ~= text.idup;
388 sax.loadStream(fl);
390 if (content is null) {
391 content = new Tag();
392 content.name = "body";
396 void loadFile (const(char)[] fname) {
397 import std.path : extension;
398 if (strEquCI(fname.extension, ".zip")) {
399 auto did = vfsAddPak!"first"(fname);
400 scope(exit) vfsRemovePak(did);
401 foreach (immutable idx, ref de; vfsFileList) {
402 if (strEquCI(de.name.extension, ".fb2")) { loadFile(vfsOpenFile(de.name)); return; }
404 throw new Exception("no fb2 file found in '"~fname.idup~"'");
405 } else {
406 loadFile(vfsOpenFile(fname));