fixed flibusta loader
[xreader.git] / xreaderfmt.d
blob23a2ee8326d4c4e7d05049903ef41233d2ab1785
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 xreaderfmt;
19 import core.time;
21 import std.concurrency;
23 import iv.strex;
25 import arsd.simpledisplay;
26 import arsd.image;
28 import iv.cmdcon;
29 import iv.nanovega;
30 import iv.nanovega.blendish;
31 import iv.nanovega.textlayouter;
32 import iv.vfs;
33 import iv.vfs.io;
35 import booktext;
37 import xreadercfg;
40 // ////////////////////////////////////////////////////////////////////////// //
41 BookText loadBook (string fname) {
42 __gshared bool eliteShipsLoaded = false;
44 import std.path;
45 //import core.memory : GC;
46 fname = fname.expandTilde.absolutePath;
47 //conwriteln("loading '", fname, "'...");
48 //GC.disable();
49 auto stt = MonoTime.currTime;
50 auto book = new BookText(fname);
51 //GC.enable();
52 //GC.collect();
53 { conwriteln("loaded: '", fname, "' in ", (MonoTime.currTime-stt).total!"msecs", " milliseconds"); }
54 currentFileId = updateFileLastReadTime(fname, book.getAuthor(), book.getTitle(),
55 book.sequence, book.seqnum);
57 if (!eliteShipsLoaded) {
58 loadEliteShips();
59 eliteShipsLoaded = true;
62 return book;
66 // ////////////////////////////////////////////////////////////////////////// //
67 private struct ImageInfo {
68 enum MaxIdleFrames = 5;
69 NVGImage iid;
70 TrueColorImage img;
71 int framesNotUsed;
74 private __gshared ImageInfo[] nvgImageIds;
77 void releaseImages (NVGContext vg) {
78 if (nvgImageIds.length) {
79 foreach (ref ImageInfo nfo; nvgImageIds) {
80 if (nfo.iid.valid) {
81 if (++nfo.framesNotUsed > ImageInfo.MaxIdleFrames) {
82 //conwriteln("freeing image with id ", nfo.iid);
83 vg.deleteImage(nfo.iid);
84 nfo.img = null;
92 private NVGImage registerImage (NVGContext vg, TrueColorImage img) {
93 if (vg is null || img is null) return NVGImage.init;
94 uint freeIdx = uint.max;
95 foreach (immutable idx, ref ImageInfo nfo; nvgImageIds) {
96 if (nfo.img is img) {
97 assert(nfo.iid.valid);
98 nfo.framesNotUsed = 0;
99 return nfo.iid;
101 if (freeIdx != uint.max && !nfo.iid.valid) freeIdx = cast(uint)idx;
103 NVGImage iid = vg.createImageFromMemoryImage(img); //vg.createImageRGBA(img.width, img.height, img.imageData.bytes[], NVGImageFlags.None);
104 if (!iid.valid) return NVGImage.init;
105 nvgImageIds ~= ImageInfo(iid, img, 0);
106 return iid;
110 class BookImage : LayObject {
111 BookText book;
112 uint imageidx;
113 TrueColorImage img;
114 this (BookText abook, uint aidx) { book = abook; imageidx = aidx; img = book.images[aidx].img; }
115 override int width () { return img.width; }
116 override int spacewidth () { return 0; }
117 override int height () { return img.height; }
118 override int ascent () { return img.height; }
119 override int descent () { return 0; }
120 override bool canbreak () { return true; }
121 override bool spaced () { return false; }
122 override void draw (NVGContext vg, float x, float y) {
123 y -= img.height;
124 vg.save();
125 scope(exit) vg.restore();
126 vg.beginPath();
127 NVGImage iid = vg.registerImage(img);
128 if (!iid.valid) return;
129 vg.fillPaint(vg.imagePattern(x, y, img.width, img.height, 0, iid));
130 //vg.globalAlpha(0.5);
131 vg.rect(x, y, img.width, img.height);
132 vg.fill();
137 // ////////////////////////////////////////////////////////////////////////// //
138 class BookMetadata {
139 static struct Anc {
140 dstring name;
141 uint wordidx; // first word
143 Anc[] sections;
144 Anc[uint] hrefs; // `name` is dest
145 Anc[] ids;
149 // ////////////////////////////////////////////////////////////////////////// //
150 final class FBFormatter {
151 private:
152 BookText book;
153 bool hasSections;
155 public:
156 LayTextC lay;
157 BookMetadata meta;
159 public:
160 this () {}
162 void formatBook (BookText abook, int maxWidth) {
163 assert(abook !is null);
165 lay = new LayTextC(laf, maxWidth);
166 lay.fontStyle.color = colorText.asUint;
168 meta = new BookMetadata();
170 book = abook;
171 scope(exit) book = null;
172 hasSections = false;
174 putMain();
176 lay.finalize();
179 private:
180 void dumpTree (Tag ct) {
181 while (ct !is null) {
182 conwriteln("tag: ", ct.name);
183 ct = ct.parent;
187 void badTag (Tag tag, string msg=null) {
188 import std.string : format, indexOf;
189 assert(tag !is null);
190 dumpTree(tag);
191 if (msg.length == 0) msg = "invalid tag: '%s'";
192 if (msg.indexOf("%s") >= 0) {
193 throw new Exception(msg.format(tag.name));
194 } else {
195 throw new Exception(msg);
199 void saveTagId (Tag tag) {
200 if (tag.id.length) {
201 import std.conv : to;
202 meta.ids ~= BookMetadata.Anc(tag.id.to!dstring, lay.nextWordIndex);
206 void registerSection (Tag tag) {
207 import std.conv : to;
208 string text = tag.textContent.xstrip;
209 //lay.sections ~= lay.curWordIndex;
210 string name;
211 while (text.length) {
212 char ch = text.ptr[0];
213 text = text[1..$];
214 if (ch <= ' ' || ch == 127) {
215 if (name.length == 0) continue;
216 if (ch != '\n') ch = ' ';
217 if (ch == '\n') {
218 if (name[$-1] > ' ') name ~= "\n...";
219 text = text.xstrip;
220 } else {
221 if (name[$-1] > ' ') name ~= ' ';
223 } else {
224 name ~= ch;
227 name = xstrip(name);
228 if (name.length == 0) name = "* * *";
229 meta.sections ~= BookMetadata.Anc(name.to!dstring, lay.nextWordIndex);
232 void putImage (Tag tag) {
233 if (tag is null || tag.href.length < 2 || tag.href[0] != '#') return;
234 string iid = tag.href[1..$];
235 //conwriteln("searching for image with href '", iid, "' (", book.images.length, ")");
236 foreach (immutable idx, ref BookText.Image img; book.images) {
237 if (iid == img.id) {
238 //conwriteln("image '", img.id, "' found");
239 lay.putObject(new BookImage(book, cast(uint)idx));
240 return;
245 void putOneTag(bool allowBad=false, bool allowText=true) (Tag tag) {
246 if (tag is null) return;
247 if (tag.name.length == 0) {
248 static if (allowText) {
249 saveTagId(tag);
250 lay.put(tag.text);
252 return;
254 switch (tag.name) {
255 case "p":
256 putTagContents(tag);
257 lay.endPara();
258 break;
260 case "strong":
261 lay.pushStyles();
262 scope(exit) lay.popStyles;
263 lay.fontStyle.bold = true;
264 putTagContents(tag);
265 break;
266 case "emphasis":
267 lay.pushStyles();
268 scope(exit) lay.popStyles;
269 lay.fontStyle.italic = true;
270 //fixFont();
271 putTagContents(tag);
272 break;
273 case "strikethrough":
274 lay.pushStyles();
275 scope(exit) lay.popStyles;
276 lay.fontStyle.strike = true;
277 //fixFont();
278 putTagContents(tag);
279 break;
281 case "empty-line":
282 lay.endLine();
283 break;
284 case "br":
285 lay.endPara();
286 break;
288 case "image":
289 lay.pushStyles();
290 scope(exit) lay.popStyles;
291 lay.endLine;
292 lay.lineStyle.setCenter;
293 putImage(tag);
294 lay.endLine;
295 return;
297 case "style":
298 //???
299 return;
301 case "a":
302 if (tag.href.length) {
303 import std.conv : to;
304 //conwriteln("href found: '", ct.href, "' (", lay.nextWordIndex, ")");
305 meta.hrefs[lay.nextWordIndex] = BookMetadata.Anc(tag.href.to!dstring, lay.nextWordIndex);
306 lay.pushStyles();
307 auto c = lay.fontStyle.color;
308 scope(exit) { lay.popStyles; lay.fontStyle.color = c; }
309 lay.fontStyle.href = true;
310 lay.fontStyle.underline = true;
311 lay.fontStyle.color = colorTextHref.asUint;
312 //fixFont();
313 putTagContents(tag);
314 } else {
315 putTagContents(tag);
317 break;
319 case "sub": // some idiotic files has this
320 case "sup": // some idiotic files has this
321 putTagContents(tag);
322 break;
324 case "code":
325 lay.pushStyles();
326 scope(exit) lay.popStyles;
327 setCodeStyle();
328 putTagContents(tag);
329 lay.endPara;
330 break;
332 case "poem":
333 lay.pushStyles();
334 scope(exit) lay.popStyles;
335 putPoem(tag);
336 //lay.endPara;
337 break;
339 case "stanza":
340 putTagContents(tag);
341 lay.endPara;
342 break;
344 case "v": // for stanzas
345 putTagContents(tag);
346 lay.endPara;
347 break;
349 case "date": // for poem
350 lay.endPara;
351 lay.pushStyles();
352 scope(exit) lay.popStyles;
353 lay.fontStyle.bold = true;
354 lay.fontStyle.italic = true;
355 lay.lineStyle.setRight;
356 //fixFont();
357 putTagContents(tag);
358 lay.endPara;
359 break;
361 case "cite":
362 putCite(tag);
363 return;
365 case "site":
366 case "annotation":
367 lay.pushStyles();
368 scope(exit) lay.popStyles;
369 lay.fontStyle.bold = true;
370 lay.lineStyle.setCenter;
371 //fixFont();
372 putTagContents(tag);
373 lay.endPara;
374 break;
376 case "text-author":
377 break;
379 case "title":
380 lay.pushStyles();
381 scope(exit) lay.popStyles;
382 setTitleStyle();
383 registerSection(tag);
384 putTagContents(tag);
385 if (tag.text.length == 0) lay.endPara();
386 break;
388 case "subtitle":
389 lay.pushStyles();
390 scope(exit) lay.popStyles;
391 setSubtitleStyle();
392 putTagContents(tag);
393 if (tag.text.length == 0) lay.endPara();
394 break;
396 case "epigraph":
397 lay.pushStyles();
398 scope(exit) lay.popStyles;
399 //auto olpad = lay.lineStyle.leftpad;
400 auto orpad = lay.lineStyle.rightpad;
401 setEpigraphStyle();
402 //lay.lineStyle.leftpad = lay.lineStyle.leftpad+olpad;
403 lay.lineStyle.rightpad = lay.lineStyle.rightpad+orpad;
404 putEpigraph(tag);
405 break;
407 case "section":
408 putTagContents(tag);
409 lay.endPara;
410 break;
412 case "table":
413 lay.pushStyles();
414 scope(exit) lay.popStyles;
415 lay.fontStyle.fontsize += 8;
416 lay.lineStyle.setCenter;
417 //fixFont();
418 lay.put("TABLE SKIPPED");
419 lay.endPara();
420 break;
422 default:
423 static if (allowBad) {
424 break;
425 } else {
426 badTag(tag);
431 bool onlyImagePara (Tag ct) {
432 if (ct is null) return false;
433 int count;
434 foreach (Tag tag; ct.children) {
435 if (tag.name.length == 0) {
436 // text
437 if (tag.text.xstrip.length != 0) return false;
438 continue;
440 if (count != 0 || tag.name != "image") return false;
441 ++count;
443 return (count == 1);
446 void putTagContents/*(bool xdump=false)*/ (Tag ct) {
447 if (ct is null) return;
448 saveTagId(ct);
449 if (onlyImagePara(ct)) {
450 lay.pushStyles();
451 scope(exit) lay.popStyles;
452 lay.endLine;
453 lay.lineStyle.setCenter;
454 foreach (Tag tag; ct.children) putOneTag(tag);
455 lay.endLine;
456 } else {
457 //if (ct.name == "subtitle") conwriteln("text: [", ct.children[0].text, "]");
458 foreach (Tag tag; ct.children) {
459 //static if (xdump) conwriteln("text: [", tag.text, "]");
460 putOneTag(tag);
463 //lay.endPara();
466 void putParas (Tag ct) {
467 if (ct is null) return;
468 putTagContents(ct);
469 lay.endPara();
472 void putAuthor (Tag ct) {
473 saveTagId(ct);
474 foreach (Tag tag; ct.children) {
475 if (tag.name.length == 0) continue;
476 if (tag.name == "text-author") {
477 putTagContents(tag);
478 lay.endPara();
483 void putCite (Tag ct) {
484 lay.pushStyles();
485 scope(exit) lay.popStyles;
486 setCiteStyle();
487 putParas(ct);
488 putAuthor(ct);
491 void putEpigraph (Tag ct) {
492 putParas(ct);
493 putAuthor(ct);
496 void putPoem (Tag ct) {
497 saveTagId(ct);
498 lay.pushStyles();
499 scope(exit) lay.popStyles;
500 setPoemStyle();
501 // put epigraph (not yet)
502 // put title and subtitle
503 foreach (Tag tag; ct.children) {
504 if (tag.name == "title") {
505 lay.pushStyles();
506 scope(exit) lay.popStyles;
507 if (!hasSections) registerSection(tag);
508 putParas(tag);
509 lay.endPara(); // space
510 } else if (tag.name == "subtitle") {
511 lay.pushStyles();
512 scope(exit) lay.popStyles;
513 putParas(tag);
514 //lay.endPara(); // space
517 lay.endPara;
518 foreach (Tag tag; ct.children) {
519 putOneTag!(false, false)(tag); // skip text without tags
521 putAuthor(ct);
524 void putMain () {
525 setNormalStyle();
526 foreach (Tag tag; book.content.children) {
527 putOneTag(tag);
531 private:
532 static public void fixFont (LayFontStash laf, ref LayFontStyle st) nothrow @safe @nogc {
533 if (st.monospace) {
534 if (st.italic) {
535 if (st.bold) st.fontface = laf.fontFaceId("monoz");
536 else st.fontface = laf.fontFaceId("monoi");
537 } else if (st.bold) {
538 if (st.italic) st.fontface = laf.fontFaceId("monoz");
539 else st.fontface = laf.fontFaceId("monob");
540 } else {
541 st.fontface = laf.fontFaceId("mono");
543 } else {
544 if (st.italic) {
545 if (st.bold) st.fontface = laf.fontFaceId("textz");
546 else st.fontface = laf.fontFaceId("texti");
547 } else if (st.bold) {
548 if (st.italic) st.fontface = laf.fontFaceId("textz");
549 else st.fontface = laf.fontFaceId("textb");
550 } else {
551 st.fontface = laf.fontFaceId("text");
556 void setNormalStyle () {
557 lay.fontStyle.resetAttrs;
558 lay.fontStyle.fontsize = fsizeText;
559 lay.fontStyle.color = colorText.asUint;
560 lay.lineStyle.leftpad = 0;
561 lay.lineStyle.rightpad = 0;
562 lay.lineStyle.paraIndent = 3;
563 if (optJustify) lay.lineStyle.setJustify; else lay.lineStyle.setLeft;
564 //fixFont();
567 void setTitleStyle () {
568 lay.fontStyle.resetAttrs;
569 lay.fontStyle.bold = true;
570 lay.fontStyle.fontsize = fsizeText+4;
571 lay.fontStyle.color = colorText.asUint;
572 lay.lineStyle.leftpad = 0;
573 lay.lineStyle.rightpad = 0;
574 lay.lineStyle.paraIndent = 0;
575 lay.lineStyle.setCenter;
576 //fixFont();
579 void setSubtitleStyle () {
580 lay.fontStyle.resetAttrs;
581 lay.fontStyle.bold = true;
582 lay.fontStyle.fontsize = fsizeText+2;
583 lay.fontStyle.color = colorText.asUint;
584 lay.lineStyle.leftpad = 0;
585 lay.lineStyle.rightpad = 0;
586 lay.lineStyle.paraIndent = 0;
587 lay.lineStyle.setCenter;
588 //fixFont();
591 void setCiteStyle () {
592 lay.fontStyle.resetAttrs;
593 lay.fontStyle.italic = true;
594 lay.fontStyle.fontsize = fsizeText;
595 lay.fontStyle.color = colorText.asUint;
596 lay.lineStyle.leftpad = fsizeText*3;
597 lay.lineStyle.rightpad = fsizeText*3;
598 lay.lineStyle.paraIndent = 3;
599 if (optJustify) lay.lineStyle.setJustify; else lay.lineStyle.setLeft;
600 //fixFont();
603 void setEpigraphStyle () {
604 lay.fontStyle.resetAttrs;
605 lay.fontStyle.italic = true;
606 lay.fontStyle.fontsize = fsizeText;
607 lay.fontStyle.color = colorText.asUint;
608 lay.lineStyle.leftpad = lay.width/3;
609 lay.lineStyle.rightpad = 0;
610 lay.lineStyle.paraIndent = 0;
611 lay.lineStyle.setRight;
612 //fixFont();
615 void setPoemStyle () {
616 lay.fontStyle.resetAttrs;
617 lay.fontStyle.italic = true;
618 lay.fontStyle.fontsize = fsizeText;
619 lay.fontStyle.color = colorText.asUint;
620 lay.lineStyle.leftpad = fsizeText*3;
621 lay.lineStyle.rightpad = 0;
622 lay.lineStyle.paraIndent = 0;
623 lay.lineStyle.setLeft;
624 //fixFont();
627 void setCodeStyle () {
628 lay.fontStyle.resetAttrs;
629 lay.fontStyle.monospace = true;
630 lay.fontStyle.italic = false;
631 lay.fontStyle.fontsize = fsizeText;
632 lay.fontStyle.color = colorText.asUint;
633 lay.lineStyle.leftpad = fsizeText*3;
634 lay.lineStyle.rightpad = fsizeText*3;
635 lay.lineStyle.paraIndent = 0;
636 lay.lineStyle.setLeft;
637 //lay.fontStyle.fontface = lay.fontFaceId("mono");
642 // ////////////////////////////////////////////////////////////////////////// //
643 private __gshared LayFontStash laf; // layouter font stash
646 // ////////////////////////////////////////////////////////////////////////// //
647 private void loadFmtFonts (/*NVGContext nvg*/) {
648 import std.functional : toDelegate;
649 if (laf is null) {
650 //if (nvg !is null) { import core.stdc.stdio : printf; printf("reusing NVG context...\n"); }
651 laf = new LayFontStash(/*nvg*/);
652 laf.fixFontDG = toDelegate(&FBFormatter.fixFont);
654 laf.addFont("text", textFontName);
655 laf.addFont("texti", textiFontName);
656 laf.addFont("textb", textbFontName);
657 laf.addFont("textz", textzFontName);
659 laf.addFont("mono", monoFontName);
660 laf.addFont("monoi", monoiFontName);
661 laf.addFont("monob", monobFontName);
662 laf.addFont("monoz", monozFontName);
667 // ////////////////////////////////////////////////////////////////////////// //
668 // messages
669 struct ReformatWork {
670 shared(BookText) booktext;
671 //shared(NVGContext) nvg; // can be null
672 string bookFileName;
673 int w, h;
676 struct ReformatWorkComplete {
677 int w, h;
678 shared(BookText) booktext;
679 shared(LayTextC) laytext;
680 shared(BookMetadata) meta;
683 struct QuitWork {
687 // ////////////////////////////////////////////////////////////////////////// //
688 void reformatThreadFn (Tid ownerTid) {
689 bool doQuit = false;
690 BookText book;
691 string newFileName;
692 int newW = -1, newH = -1;
693 //NVGContext lastnvg = null;
694 while (!doQuit) {
695 book = null;
696 newFileName = null;
697 newW = newH = -1;
698 receive(
699 (ReformatWork w) {
700 //{ conwriteln("reformat request received..."); }
701 book = cast(BookText)w.booktext;
702 newFileName = w.bookFileName;
703 newW = w.w;
704 newH = w.h;
705 if (newW < 1) newW = 1;
706 if (newH < 1) newH = 1;
707 //lastnvg = cast(NVGContext)w.nvg;
709 (QuitWork w) {
710 doQuit = true;
713 if (!doQuit && newW > 0 && newH > 0) {
714 try {
715 if (book is null) {
716 //conwriteln("loading new book: '", newFileName, "'");
717 book = loadBook(newFileName);
720 int maxWidth = newW-4-2-BND_SCROLLBAR_WIDTH-2;
721 if (maxWidth < 64) maxWidth = 64;
723 loadFmtFonts();
725 // layout text
726 //conwriteln("layouting...");
727 auto fmtr = new FBFormatter();
729 auto stt = MonoTime.currTime;
731 auto lay = new LayText(laf, maxWidth);
732 lay.fontStyle.color = colorText.asUint;
733 auto meta = book.formatBook(lay);
735 fmtr.formatBook(book, maxWidth);
736 auto ett = MonoTime.currTime-stt;
738 auto meta = fmtr.meta;
739 auto lay = fmtr.lay;
741 conwriteln("layouted in ", ett.total!"msecs", " milliseconds");
742 auto res = ReformatWorkComplete(newW, newH, cast(shared)book, cast(shared)lay, cast(shared)meta);
743 send(ownerTid, res);
744 } catch (Throwable e) {
745 // here, we are dead and fucked (the exact order doesn't matter)
746 import core.stdc.stdlib : abort;
747 import core.stdc.stdio : fprintf, stderr;
748 import core.memory : GC;
749 import core.thread : thread_suspendAll;
750 GC.disable(); // yeah
751 thread_suspendAll(); // stop right here, you criminal scum!
752 auto s = e.toString();
753 fprintf(stderr, "\n=== FATAL ===\n%.*s\n", cast(uint)s.length, s.ptr);
754 abort(); // die, you bitch!
758 send(ownerTid, QuitWork());