more rendering options as convars
[xreader.git] / xreaderfmt.d
blob6d3a9659f1e1c393a92cfae2874cf078c6f38c00
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.nanovg;
29 import iv.nanovg.oui.blendish;
30 import iv.vfs;
31 import iv.vfs.io;
33 import booktext;
35 import xreadercfg;
36 import xlayouter;
39 // ////////////////////////////////////////////////////////////////////////// //
40 BookText loadBook (string fname) {
41 __gshared bool eliteShipsLoaded = false;
43 import std.path;
44 //import core.memory : GC;
45 fname = fname.expandTilde.absolutePath;
46 //writeln("loading '", fname, "'...");
47 //GC.disable();
48 auto stt = MonoTime.currTime;
49 auto book = new BookText(fname);
50 //GC.enable();
51 //GC.collect();
52 { import std.stdio; writeln("loaded: '", fname, "' in ", (MonoTime.currTime-stt).total!"msecs", " milliseconds"); }
54 string[] lines;
55 string[] comments;
56 try {
57 foreach (string line; VFile(buildPath(RcDir, ".lastfile")).byLineCopy) {
58 if (line != fname) {
59 lines ~= line;
60 } else {
61 while (lines.length && lines[$-1].isComment) {
62 comments ~= lines[$-1];
63 lines = lines[0..$-1];
67 } catch (Exception) {}
68 try { import std.file; mkdirRecurse(RcDir); } catch (Exception) {}
69 auto fo = VFile(buildPath(RcDir, ".lastfile"), "w");
70 foreach (string s; lines) fo.writeln(s);
71 foreach_reverse (string s; comments) fo.writeln(s);
72 fo.writeln(fname);
74 stateFileName = buildPath(RcDir, fname.baseName~".rc");
76 if (!eliteShipsLoaded) {
77 loadEliteShips();
78 eliteShipsLoaded = true;
81 return book;
85 // ////////////////////////////////////////////////////////////////////////// //
86 private struct ImageInfo {
87 enum MaxIdleFrames = 5;
88 int iid;
89 TrueColorImage img;
90 int framesNotUsed;
93 private __gshared ImageInfo[] nvgImageIds;
96 void releaseImages (NVGContext vg) {
97 if (nvgImageIds.length) {
98 foreach (ref ImageInfo nfo; nvgImageIds) {
99 if (nfo.iid >= 0) {
100 if (++nfo.framesNotUsed > ImageInfo.MaxIdleFrames) {
101 //writeln("freeing image with id ", nfo.iid);
102 vg.deleteImage(nfo.iid);
103 nfo.iid = -1;
104 nfo.img = null;
112 private int registerImage (NVGContext vg, TrueColorImage img) {
113 if (vg is null || img is null) return -1;
114 uint freeIdx = uint.max;
115 foreach (immutable idx, ref ImageInfo nfo; nvgImageIds) {
116 if (nfo.img is img) {
117 assert(nfo.iid >= 0);
118 nfo.framesNotUsed = 0;
119 return nfo.iid;
121 if (freeIdx != uint.max && nfo.iid < 0) freeIdx = cast(uint)idx;
123 int iid = vg.createImageFromMemoryImage(img); //vg.createImageRGBA(img.width, img.height, img.imageData.bytes[], NVGImageFlags.None);
124 if (iid < 0) return -1;
125 nvgImageIds ~= ImageInfo(iid, img, 0);
126 return iid;
130 class BookImage : LayObject {
131 BookText book;
132 uint imageidx;
133 TrueColorImage img;
134 this (BookText abook, uint aidx) { book = abook; imageidx = aidx; img = book.images[aidx].img; }
135 override int width () { return img.width; }
136 override int spacewidth () { return 0; }
137 override int height () { return img.height; }
138 override int ascent () { return img.height; }
139 override int descent () { return 0; }
140 override bool canbreak () { return true; }
141 override bool spaced () { return false; }
142 override void draw (NVGContext vg, float x, float y) {
143 y -= img.height;
144 vg.save();
145 scope(exit) vg.restore();
146 vg.beginPath();
147 int iid = vg.registerImage(img);
148 if (iid < 0) return;
149 vg.fillPaint(vg.imagePattern(x, y, img.width, img.height, 0, iid));
150 //vg.globalAlpha(0.5);
151 vg.rect(x, y, img.width, img.height);
152 vg.fill();
157 // ////////////////////////////////////////////////////////////////////////// //
158 class BookMetadata {
159 static struct Anc {
160 dstring name;
161 uint wordidx; // first word
163 Anc[] sections;
164 Anc[uint] hrefs; // `name` is dest
165 Anc[] ids;
169 // ////////////////////////////////////////////////////////////////////////// //
170 private void fixFont (LayText lay) {
171 if (lay.fontStyle.italic) {
172 if (lay.fontStyle.bold) lay.fontStyle.fontface = lay.fontFaceId("textz");
173 else lay.fontStyle.fontface = lay.fontFaceId("texti");
174 } else if (lay.fontStyle.bold) {
175 if (lay.fontStyle.italic) lay.fontStyle.fontface = lay.fontFaceId("textz");
176 else lay.fontStyle.fontface = lay.fontFaceId("textb");
177 } else {
178 lay.fontStyle.fontface = lay.fontFaceId("text");
182 private void setNormalStyle (LayText lay) {
183 lay.fontStyle.resetAttrs;
184 lay.fontStyle.fontsize = fsizeText;
185 lay.fontStyle.color = colorText.asUint;
186 lay.lineStyle.leftpad = 0;
187 lay.lineStyle.rightpad = 0;
188 lay.lineStyle.paraIndent = 3;
189 if (optJustify) lay.lineStyle.setJustify; else lay.lineStyle.setLeft;
190 fixFont(lay);
193 private void setTitleStyle (LayText lay) {
194 lay.fontStyle.resetAttrs;
195 lay.fontStyle.bold = true;
196 lay.fontStyle.fontsize = fsizeText+4;
197 lay.fontStyle.color = colorText.asUint;
198 lay.lineStyle.leftpad = 0;
199 lay.lineStyle.rightpad = 0;
200 lay.lineStyle.paraIndent = 0;
201 lay.lineStyle.setCenter;
202 fixFont(lay);
205 private void setSubtitleStyle (LayText lay) {
206 lay.fontStyle.resetAttrs;
207 lay.fontStyle.bold = true;
208 lay.fontStyle.fontsize = fsizeText+2;
209 lay.fontStyle.color = colorText.asUint;
210 lay.lineStyle.leftpad = 0;
211 lay.lineStyle.rightpad = 0;
212 lay.lineStyle.paraIndent = 0;
213 lay.lineStyle.setCenter;
214 fixFont(lay);
217 private void setCiteStyle (LayText lay) {
218 lay.fontStyle.resetAttrs;
219 lay.fontStyle.italic = true;
220 lay.fontStyle.fontsize = fsizeText;
221 lay.fontStyle.color = colorText.asUint;
222 lay.lineStyle.leftpad = fsizeText*3;
223 lay.lineStyle.rightpad = fsizeText*3;
224 lay.lineStyle.paraIndent = 3;
225 if (optJustify) lay.lineStyle.setJustify; else lay.lineStyle.setLeft;
226 fixFont(lay);
229 private void setEpigraphStyle (LayText lay) {
230 lay.fontStyle.resetAttrs;
231 lay.fontStyle.italic = true;
232 lay.fontStyle.fontsize = fsizeText;
233 lay.fontStyle.color = colorText.asUint;
234 lay.lineStyle.leftpad = lay.width/3;
235 lay.lineStyle.rightpad = 0;
236 lay.lineStyle.paraIndent = 0;
237 lay.lineStyle.setRight;
238 fixFont(lay);
241 private void setPoemStyle (LayText lay) {
242 lay.fontStyle.resetAttrs;
243 lay.fontStyle.italic = true;
244 lay.fontStyle.fontsize = fsizeText;
245 lay.fontStyle.color = colorText.asUint;
246 lay.lineStyle.leftpad = fsizeText*3;
247 lay.lineStyle.rightpad = 0;
248 lay.lineStyle.paraIndent = 0;
249 lay.lineStyle.setLeft;
250 fixFont(lay);
254 // ////////////////////////////////////////////////////////////////////////// //
255 public BookMetadata formatBook (BookText book, LayText lay) {
256 assert(book !is null);
258 BookMetadata meta = new BookMetadata();
260 void dumpTree (Tag ct) {
261 while (ct !is null) {
262 { import std.stdio; writeln("tag: ", ct.name); }
263 ct = ct.parent;
267 void badTag (Tag tag, string msg=null) {
268 import std.string : format, indexOf;
269 assert(tag !is null);
270 dumpTree(tag);
271 if (msg.length == 0) msg = "invalid tag: '%s'";
272 if (msg.indexOf("%s") >= 0) {
273 throw new Exception(msg.format(tag.name));
274 } else {
275 throw new Exception(msg);
279 void saveTagId (Tag tag) {
280 if (tag.id.length) {
281 import std.conv : to;
282 meta.ids ~= BookMetadata.Anc(tag.id.to!dstring, lay.nextWordIndex);
286 void putImage (Tag tag) {
287 if (tag is null || tag.href.length < 2 || tag.href[0] != '#') return;
288 string iid = tag.href[1..$];
289 //writeln("searching for image with href '", iid, "' (", book.images.length, ")");
290 foreach (immutable idx, ref BookText.Image img; book.images) {
291 if (iid == img.id) {
292 //writeln("image '", img.id, "' found");
293 lay.putObject(new BookImage(book, cast(uint)idx));
294 return;
299 void putParaContentInternal (Tag ct, int boldc=0, int italicc=0, int underc=0) {
300 if (ct is null) return;
301 auto c = lay.fontStyle.color;
302 scope(exit) lay.fontStyle.color = c;
303 saveTagId(ct);
304 switch (ct.name) {
305 case "strong": ++boldc; lay.fontStyle.bold = true; break;
306 case "emphasis": ++italicc; lay.fontStyle.italic = true; break;
307 case "image": putImage(ct); return;
308 case "style": return;
309 case "a":
310 if (ct.href.length) {
311 import std.conv : to;
312 //writeln("href found: '", ct.href, "' (", lay.nextWordIndex, ")");
313 meta.hrefs[lay.nextWordIndex] = BookMetadata.Anc(ct.href.to!dstring, lay.nextWordIndex);
314 ++underc;
315 lay.fontStyle.href = true;
316 lay.fontStyle.underline = true;
317 lay.fontStyle.color = colorTextHref.asUint;
319 break;
320 case "sub": break; // some idiotic files has this
321 case "sup": break; // some idiotic files has this
322 case "": lay.put(ct.text); return;
323 default: badTag(ct);
325 fixFont(lay);
326 foreach (Tag tag; ct.children) putParaContentInternal(tag, boldc, italicc, underc);
327 switch (ct.name) {
328 case "strong": if (--boldc == 0) lay.fontStyle.bold = false; break;
329 case "emphasis": if (--italicc == 0) lay.fontStyle.italic = false; break;
330 case "a": if (--underc == 0) lay.fontStyle.underline = false; lay.fontStyle.href = false; break;
331 default:
333 fixFont(lay);
336 bool onlyImagePara (Tag ct) {
337 if (ct is null) return false;
338 int count;
339 foreach (Tag tag; ct.children) {
340 if (tag.name.length == 0) {
341 // text
342 if (tag.text.xstrip.length != 0) return false;
343 continue;
345 if (count != 0 || tag.name != "image") return false;
346 ++count;
348 return (count == 1);
351 void putTagContents (Tag ct, int boldc=0, int italicc=0, int underc=0) {
352 if (ct is null) return;
353 saveTagId(ct);
354 if (onlyImagePara(ct)) {
355 lay.pushStyles();
356 scope(exit) lay.popStyles;
357 lay.lineStyle.setCenter;
358 foreach (Tag tag; ct.children) putParaContentInternal(tag, boldc, italicc, underc);
359 } else {
360 foreach (Tag tag; ct.children) putParaContentInternal(tag, boldc, italicc, underc);
362 lay.endPara();
365 void putParas (Tag ct) {
366 saveTagId(ct);
367 foreach (Tag tag; ct.children) {
368 if (tag.name.length == 0) continue;
369 if (tag.name == "p") putTagContents(tag);
370 else if (tag.name == "empty-line") lay.endLine();
371 else if (tag.name == "text-author") {}
372 else if (tag.name == "strong") putTagContents(tag, 1);
373 else badTag(tag);
377 void putAuthor (Tag ct) {
378 saveTagId(ct);
379 foreach (Tag tag; ct.children) {
380 if (tag.name.length == 0) continue;
381 if (tag.name == "text-author") {
382 putTagContents(tag);
383 lay.endPara();
388 void putCite (Tag ct) {
389 lay.pushStyles();
390 scope(exit) lay.popStyles;
391 setCiteStyle(lay);
392 putParas(ct);
393 putAuthor(ct);
396 void putEpigraph (Tag ct) {
397 lay.pushStyles();
398 scope(exit) lay.popStyles;
399 setEpigraphStyle(lay);
400 putParas(ct);
401 putAuthor(ct);
404 void putStanza (Tag ct) {
405 saveTagId(ct);
406 foreach (Tag tag; ct.children) {
407 if (tag.name.length == 0) continue;
408 if (tag.name == "text-author") continue;
409 if (tag.name == "title") badTag(tag, "titles in stanzas are not supported yet");
410 if (tag.name == "subtitle") badTag(tag, "subtitles in stanzas are not supported yet");
411 if (tag.name == "epigraph") badTag(tag, "epigraphs in poems are not supported yet");
412 if (tag.name == "date") continue;
413 if (tag.name == "v") { putTagContents(tag); continue; }
414 badTag(tag);
418 void putPoem (Tag ct) {
419 saveTagId(ct);
420 lay.pushStyles();
421 scope(exit) lay.popStyles;
422 setPoemStyle(lay);
423 // put epigraph (not yet)
424 // put title and subtitle
425 foreach (Tag tag; ct.children) {
426 if (tag.name == "title") {
427 lay.pushStyles();
428 scope(exit) lay.popStyles;
429 putParas(tag);
430 lay.endPara(); // space
431 } else if (tag.name == "subtitle") {
432 lay.pushStyles();
433 scope(exit) lay.popStyles;
434 putParas(tag);
435 //lay.endPara(); // space
438 lay.endPara;
439 foreach (Tag tag; ct.children) {
440 if (tag.name.length == 0) continue;
441 if (tag.name == "text-author") continue;
442 if (tag.name == "title") continue;
443 if (tag.name == "subtitle") continue;
444 if (tag.name == "epigraph") badTag(tag, "epigraphs in poems are not supported yet");
445 if (tag.name == "date") continue;
446 if (tag.name == "stanza") { putStanza(tag); lay.put(LayText.EndParaCh); continue; }
447 badTag(tag);
449 putAuthor(ct);
452 void putSection (Tag sc) {
453 bool sectionRegistered = false;
455 void registerSection (Tag tag) {
456 import std.conv : to;
457 if (sectionRegistered) return;
458 sectionRegistered = true;
459 string text = tag.textContent.xstrip;
460 //lay.sections ~= lay.curWordIndex;
461 string name;
462 while (text.length) {
463 char ch = text.ptr[0];
464 text = text[1..$];
465 if (ch <= ' ' || ch == 127) {
466 if (name.length == 0) continue;
467 if (ch != '\n') ch = ' ';
468 if (ch == '\n') {
469 if (name[$-1] > ' ') name ~= "\n...";
470 text = text.xstrip;
471 } else {
472 if (name[$-1] > ' ') name ~= ' ';
474 } else {
475 name ~= ch;
478 name = xstrip(name);
479 if (name.length == 0) name = "* * *";
480 meta.sections ~= BookMetadata.Anc(name.to!dstring, lay.nextWordIndex);
483 saveTagId(sc);
484 foreach (Tag tag; sc.children) {
485 if (tag.name.length == 0) continue;
486 if (tag.name == "title") {
487 lay.pushStyles();
488 scope(exit) lay.popStyles;
489 setTitleStyle(lay);
490 registerSection(tag);
491 putParas(tag);
492 } else if (tag.name == "subtitle") {
493 lay.pushStyles();
494 scope(exit) lay.popStyles;
495 setSubtitleStyle(lay);
496 registerSection(tag);
497 putParas(tag);
498 } else if (tag.name == "epigraph") {
499 lay.pushStyles();
500 scope(exit) lay.popStyles;
501 setEpigraphStyle(lay);
502 putEpigraph(tag);
503 } else if (tag.name == "section") {
504 lay.pushStyles();
505 scope(exit) lay.popStyles;
506 setNormalStyle(lay);
507 putSection(tag);
508 } else if (tag.name == "p") {
509 putTagContents(tag);
510 } else if (tag.name == "image") {
511 lay.pushStyles();
512 scope(exit) lay.popStyles;
513 lay.lineStyle.leftpad = 0;
514 lay.lineStyle.rightpad = 0;
515 lay.lineStyle.paraIndent = 0;
516 lay.lineStyle.setCenter;
517 putImage(tag);
518 lay.endPara();
519 } else if (tag.name == "empty-line") {
520 lay.endPara();
521 } else if (tag.name == "cite") {
522 putCite(tag);
523 } else if (tag.name == "table") {
524 lay.pushStyles();
525 scope(exit) lay.popStyles;
526 lay.fontStyle.fontsize += 8;
527 lay.lineStyle.setCenter;
528 lay.put("TABLE SKIPPED");
529 lay.endPara();
530 } else if (tag.name == "poem") {
531 putPoem(tag);
532 } else if (tag.name == "image") {
533 } else if (tag.name == "style") {
534 } else {
535 badTag(tag);
538 lay.endPara;
541 foreach (Tag tag; book.content.children) {
542 if (tag.name == "title") {
543 lay.pushStyles();
544 scope(exit) lay.popStyles;
545 setTitleStyle(lay);
546 lay.endPara();
547 putParas(tag);
548 lay.endPara();
549 } else if (tag.name == "subtitle") {
550 lay.pushStyles();
551 scope(exit) lay.popStyles;
552 setTitleStyle(lay);
553 putParas(tag);
554 lay.endPara();
555 } else if (tag.name == "epigraph") {
556 putEpigraph(tag);
557 } else if (tag.name == "section") {
558 lay.pushStyles();
559 scope(exit) lay.popStyles;
560 setNormalStyle(lay);
561 putSection(tag);
562 } else if (tag.name == "image") {
563 putImage(tag);
567 lay.finalize();
569 return meta;
573 // ////////////////////////////////////////////////////////////////////////// //
574 private __gshared LayFontStash laf; // layouter font stash
577 // ////////////////////////////////////////////////////////////////////////// //
578 private void loadFmtFonts () {
579 laf = new LayFontStash();
581 laf.addFont("text", textFontName);
582 laf.addFont("texti", textiFontName);
583 laf.addFont("textb", textbFontName);
584 laf.addFont("textz", textzFontName);
586 laf.addFont("mono", monoFontName);
587 laf.addFont("monoi", monoiFontName);
588 laf.addFont("monob", monobFontName);
589 laf.addFont("monoz", monozFontName);
593 // ////////////////////////////////////////////////////////////////////////// //
594 // messages
595 struct ReformatWork {
596 shared(BookText) booktext;
597 string bookFileName;
598 int w, h;
601 struct ReformatWorkComplete {
602 int w, h;
603 shared(BookText) booktext;
604 shared(LayText) laytext;
605 shared(BookMetadata) meta;
608 struct QuitWork {
612 // ////////////////////////////////////////////////////////////////////////// //
613 void reformatThreadFn (Tid ownerTid) {
614 bool doQuit = false;
615 loadFmtFonts();
616 BookText book;
617 string newFileName;
618 int newW = -1, newH = -1;
619 while (!doQuit) {
620 book = null;
621 newFileName = null;
622 newW = newH = -1;
623 receive(
624 (ReformatWork w) {
625 //{ import std.stdio; writeln("reformat request received..."); }
626 book = cast(BookText)w.booktext;
627 newFileName = w.bookFileName;
628 newW = w.w;
629 newH = w.h;
630 if (newW < 1) newW = 1;
631 if (newH < 1) newH = 1;
633 (QuitWork w) {
634 doQuit = true;
637 if (!doQuit && newW > 0 && newH > 0) {
638 try {
639 if (book is null) {
640 //writeln("loading new book: '", newFileName, "'");
641 book = loadBook(newFileName);
644 int maxWidth = newW-4-2-BND_SCROLLBAR_WIDTH-2;
645 if (maxWidth < 64) maxWidth = 64;
647 // layout text
648 //writeln("layouting...");
649 auto stt = MonoTime.currTime;
650 auto lay = new LayText(laf, maxWidth);
651 lay.fontStyle.color = colorText.asUint;
653 auto meta = book.formatBook(lay);
655 auto ett = MonoTime.currTime-stt;
656 writeln("layouted in ", ett.total!"msecs", " milliseconds");
657 auto res = ReformatWorkComplete(newW, newH, cast(shared)book, cast(shared)lay, cast(shared)meta);
658 send(ownerTid, res);
659 } catch (Throwable e) {
660 // here, we are dead and fucked (the exact order doesn't matter)
661 import core.stdc.stdlib : abort;
662 import core.stdc.stdio : fprintf, stderr;
663 import core.memory : GC;
664 import core.thread : thread_suspendAll;
665 GC.disable(); // yeah
666 thread_suspendAll(); // stop right here, you criminal scum!
667 auto s = e.toString();
668 fprintf(stderr, "\n=== FATAL ===\n%.*s\n", cast(uint)s.length, s.ptr);
669 abort(); // die, you bitch!
673 send(ownerTid, QuitWork());