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*/;
26 //import iv.iresample;
28 import iv
.nanovega
.textlayouter
;
35 // ////////////////////////////////////////////////////////////////////////// //
41 string diskfile
; // not set by `loadBookInfo()`!
48 BookInfo
loadBookInfo (const(char)[] fname
) {
49 static BookInfo
loadFile (VFile fl
) {
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];
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
) {
68 res
.seqnum
= to
!uint(*id
);
69 if (res
.seqnum
< 1 || res
.seqnum
> 8192) res
.seqname
= null;
74 if (res
.seqname
.length
== 0) { res
.seqname
= null; res
.seqnum
= 0; }
77 sax
.onClose("/FictionBook/description", (char[] text
) {
79 throw new Exception("done"); // sorry
82 // author's first name
83 sax
.onContent("/FictionBook/description/title-info/author/first-name", (char[] text
) {
84 authorFirst
= strip(text
).idup
;
87 sax
.onContent("/FictionBook/description/title-info/author/last-name", (char[] text
) {
88 authorLast
= strip(text
).idup
;
91 sax
.onContent("/FictionBook/description/title-info/book-title", (char[] text
) {
92 res
.title
= strip(text
).idup
;
96 } catch (Exception e
) {
97 if (!complete
) throw e
;
99 if (authorFirst
.length
== 0) {
100 authorFirst
= authorLast
;
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
;
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
~"'");
119 return loadFile(vfsOpenFile(fname
));
124 // ////////////////////////////////////////////////////////////////////////// //
126 string name
; // empty: text node
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
;
141 foreach (const Tag t
; children
) res
~= t
.textContent
;
145 string
toStringNice (int indent
=0) const {
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
;
151 if (children
.length
== 0) res
~= '/';
153 if (id
.length
) { res
~= " id="; res
~= id
; }
154 if (href
.length
) { res
~= " href="; res
~= href
; }
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();
171 override string
toString () const { return toStringNice(int.min
); }
175 // ////////////////////////////////////////////////////////////////////////// //
176 final class BookText
{
183 static struct Image
{
192 this (const(char)[] fname
) { loadFile(fname
); }
193 this (VFile fl
) { loadFile(fl
); }
195 string
getAuthor () {
196 if (authorLast
.length
!= 0) return authorFirst
~" "~authorLast
;
200 string
getTitle () => title
;
203 void loadFile (VFile fl
) {
204 auto sax
= new SaxyEx();
206 string
norm (const(char)[] s
) {
208 foreach (char ch
; s
) {
209 if (ch
<= ' ' || ch
== 127) {
210 if (res
.length
&& res
[$-1] > ' ') res
~= ch
;
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;
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
);
242 sax
.onContent("/FictionBook/description/title-info/book-title", (char[] text
) {
246 if (authorFirst
.length
== 0) {
247 authorFirst
= authorLast
;
255 sax
.onOpen("/FictionBook/binary", (char[] text
, char[][string
] attrs
) {
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");
264 imagefmt
= (*ctp
).idup
;
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
) {
279 if (imageid
.length
) {
280 //conwriteln("image with id '", imageid, "'...");
282 if (imagectx
.length
== 0) {
283 conwriteln("image with id '", imageid
, "' has no data");
286 //auto imgdata = Base64.decode(imagectx);
287 auto imgdata
= base64Decode(imagectx
);
290 if (imagefmt
== "image/jpeg" || imagefmt
== "image/jpg") {
291 memimg
= readJpegFromMemory(imgdata
[]);
292 } else if (imagefmt
== "image/png") {
293 memimg
= imageFromPng(readPng(imgdata
[]));
297 if (memimg
is null) {
298 conwriteln("fucked image, id '", imageid
, "'");
300 img
= cast(TrueColorImage
)memimg
;
302 img
= memimg
.getAsTrueColorImage
;
305 if (img
.width
> 1 && img
.height
> 1) {
306 if (img
.width
> 1024 || img
.height
> 1024) {
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
);
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
);
332 sax
.onOpen("/FictionBook/body", (char[] text
) {
333 if (content
is null) {
335 content
.name
= "body";
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") {
363 conwriteln("IMAGE: ", tag.href);
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();
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
;
390 if (content
is null) {
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
~"'");
406 loadFile(vfsOpenFile(fname
));