more smaller data types -- memory usage on "astarta" dropped to 46MB (was 67MB)
[xreader.git] / xreaderfmt.d
blobcf6fa13f5764df46b64507153000f71526dc7c60
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.color;
27 import arsd.png;
28 import arsd.jpeg;
30 import iv.nanovg;
31 import iv.nanovg.oui.blendish;
32 import iv.vfs;
33 import iv.vfs.io;
35 import booktext;
37 import xreadercfg;
38 import xlayouter;
41 // ////////////////////////////////////////////////////////////////////////// //
42 BookText loadBook (string fname) {
43 __gshared bool eliteShipsLoaded = false;
45 import std.path;
46 //import core.memory : GC;
47 fname = fname.expandTilde.absolutePath;
48 //writeln("loading '", fname, "'...");
49 //GC.disable();
50 auto stt = MonoTime.currTime;
51 auto book = new BookText(fname);
52 //GC.enable();
53 //GC.collect();
54 { import std.stdio; writeln("loaded: '", fname, "' in ", (MonoTime.currTime-stt).total!"msecs", " milliseconds"); }
56 string[] lines;
57 string[] comments;
58 try {
59 foreach (string line; VFile(buildPath(RcDir, ".lastfile")).byLineCopy) {
60 if (line != fname) {
61 lines ~= line;
62 } else {
63 while (lines.length && lines[$-1].isComment) {
64 comments ~= lines[$-1];
65 lines = lines[0..$-1];
69 } catch (Exception) {}
70 try { import std.file; mkdirRecurse(RcDir); } catch (Exception) {}
71 auto fo = VFile(buildPath(RcDir, ".lastfile"), "w");
72 foreach (string s; lines) fo.writeln(s);
73 foreach_reverse (string s; comments) fo.writeln(s);
74 fo.writeln(fname);
76 stateFileName = buildPath(RcDir, fname.baseName~".rc");
78 if (!eliteShipsLoaded) {
79 loadEliteShips();
80 eliteShipsLoaded = true;
83 return book;
87 // ////////////////////////////////////////////////////////////////////////// //
88 private struct ImageInfo {
89 enum MaxIdleFrames = 5;
90 int iid;
91 TrueColorImage img;
92 int framesNotUsed;
95 private __gshared ImageInfo[] nvgImageIds;
98 void releaseImages (NVGContext vg) {
99 if (nvgImageIds.length) {
100 foreach (ref ImageInfo nfo; nvgImageIds) {
101 if (nfo.iid >= 0) {
102 if (++nfo.framesNotUsed > ImageInfo.MaxIdleFrames) {
103 //writeln("freeing image with id ", nfo.iid);
104 vg.deleteImage(nfo.iid);
105 nfo.iid = -1;
106 nfo.img = null;
114 private int registerImage (NVGContext vg, TrueColorImage img) {
115 if (vg is null || img is null) return -1;
116 uint freeIdx = uint.max;
117 foreach (immutable idx, ref ImageInfo nfo; nvgImageIds) {
118 if (nfo.img is img) {
119 assert(nfo.iid >= 0);
120 nfo.framesNotUsed = 0;
121 return nfo.iid;
123 if (freeIdx != uint.max && nfo.iid < 0) freeIdx = cast(uint)idx;
125 int iid = vg.createImageFromMemoryImage(img); //vg.createImageRGBA(img.width, img.height, img.imageData.bytes[], NVGImageFlags.None);
126 if (iid < 0) return -1;
127 nvgImageIds ~= ImageInfo(iid, img, 0);
128 return iid;
132 class BookImage : LayObject {
133 BookText book;
134 uint imageidx;
135 TrueColorImage img;
136 this (BookText abook, uint aidx) { book = abook; imageidx = aidx; img = book.images[aidx].img; }
137 override int width () { return img.width; }
138 override int spacewidth () { return 0; }
139 override int height () { return img.height; }
140 override int ascent () { return img.height; }
141 override int descent () { return 0; }
142 override bool canbreak () { return true; }
143 override bool spaced () { return false; }
144 override void draw (NVGContext vg, float x, float y) {
145 y -= img.height;
146 vg.save();
147 scope(exit) vg.restore();
148 vg.beginPath();
149 int iid = vg.registerImage(img);
150 if (iid < 0) return;
151 vg.fillPaint(vg.imagePattern(x, y, img.width, img.height, 0, iid));
152 //vg.globalAlpha(0.5);
153 vg.rect(x, y, img.width, img.height);
154 vg.fill();
159 // ////////////////////////////////////////////////////////////////////////// //
160 class BookMetadata {
161 static struct Anc {
162 dstring name;
163 uint wordidx; // first word
165 Anc[] sections;
166 Anc[uint] hrefs; // `name` is dest
167 Anc[] ids;
171 // ////////////////////////////////////////////////////////////////////////// //
172 private void fixFont (LayText lay) {
173 if (lay.fontStyle.italic) {
174 if (lay.fontStyle.bold) lay.fontStyle.fontface = lay.fontFaceId("textz");
175 else lay.fontStyle.fontface = lay.fontFaceId("texti");
176 } else if (lay.fontStyle.bold) {
177 if (lay.fontStyle.italic) lay.fontStyle.fontface = lay.fontFaceId("textz");
178 else lay.fontStyle.fontface = lay.fontFaceId("textb");
179 } else {
180 lay.fontStyle.fontface = lay.fontFaceId("text");
184 private void setNormalStyle (LayText lay) {
185 lay.fontStyle.resetAttrs;
186 lay.fontStyle.fontsize = fsizeText;
187 lay.fontStyle.color = colorText.asUint;
188 lay.lineStyle.leftpad = 0;
189 lay.lineStyle.rightpad = 0;
190 lay.lineStyle.paraIndent = 3;
191 if (optJustify) lay.lineStyle.setJustify; else lay.lineStyle.setLeft;
192 fixFont(lay);
195 private void setTitleStyle (LayText lay) {
196 lay.fontStyle.resetAttrs;
197 lay.fontStyle.bold = true;
198 lay.fontStyle.fontsize = fsizeText+4;
199 lay.fontStyle.color = colorText.asUint;
200 lay.lineStyle.leftpad = 0;
201 lay.lineStyle.rightpad = 0;
202 lay.lineStyle.paraIndent = 0;
203 lay.lineStyle.setCenter;
204 fixFont(lay);
207 private void setSubtitleStyle (LayText lay) {
208 lay.fontStyle.resetAttrs;
209 lay.fontStyle.bold = true;
210 lay.fontStyle.fontsize = fsizeText+2;
211 lay.fontStyle.color = colorText.asUint;
212 lay.lineStyle.leftpad = 0;
213 lay.lineStyle.rightpad = 0;
214 lay.lineStyle.paraIndent = 0;
215 lay.lineStyle.setCenter;
216 fixFont(lay);
219 private void setCiteStyle (LayText lay) {
220 lay.fontStyle.resetAttrs;
221 lay.fontStyle.italic = true;
222 lay.fontStyle.fontsize = fsizeText;
223 lay.fontStyle.color = colorText.asUint;
224 lay.lineStyle.leftpad = fsizeText*3;
225 lay.lineStyle.rightpad = fsizeText*3;
226 lay.lineStyle.paraIndent = 3;
227 if (optJustify) lay.lineStyle.setJustify; else lay.lineStyle.setLeft;
228 fixFont(lay);
231 private void setEpigraphStyle (LayText lay) {
232 lay.fontStyle.resetAttrs;
233 lay.fontStyle.italic = true;
234 lay.fontStyle.fontsize = fsizeText;
235 lay.fontStyle.color = colorText.asUint;
236 lay.lineStyle.leftpad = lay.width/2;
237 lay.lineStyle.rightpad = 0;
238 lay.lineStyle.paraIndent = 0;
239 lay.lineStyle.setRight;
240 fixFont(lay);
243 private void setPoemStyle (LayText lay) {
244 lay.fontStyle.resetAttrs;
245 lay.fontStyle.italic = true;
246 lay.fontStyle.fontsize = fsizeText;
247 lay.fontStyle.color = colorText.asUint;
248 lay.lineStyle.leftpad = fsizeText*3;
249 lay.lineStyle.rightpad = 0;
250 lay.lineStyle.paraIndent = 0;
251 lay.lineStyle.setLeft;
252 fixFont(lay);
256 // ////////////////////////////////////////////////////////////////////////// //
257 public BookMetadata formatBook (BookText book, LayText lay) {
258 assert(book !is null);
260 BookMetadata meta = new BookMetadata();
262 void dumpTree (Tag ct) {
263 while (ct !is null) {
264 { import std.stdio; writeln("tag: ", ct.name); }
265 ct = ct.parent;
269 void badTag (Tag tag, string msg=null) {
270 import std.string : format, indexOf;
271 assert(tag !is null);
272 dumpTree(tag);
273 if (msg.length == 0) msg = "invalid tag: '%s'";
274 if (msg.indexOf("%s") >= 0) {
275 throw new Exception(msg.format(tag.name));
276 } else {
277 throw new Exception(msg);
281 void saveTagId (Tag tag) {
282 if (tag.id.length) {
283 import std.conv : to;
284 meta.ids ~= BookMetadata.Anc(tag.id.to!dstring, lay.nextWordIndex);
288 void putImage (Tag tag) {
289 if (tag is null || tag.href.length < 2 || tag.href[0] != '#') return;
290 string iid = tag.href[1..$];
291 //writeln("searching for image with href '", iid, "' (", book.images.length, ")");
292 foreach (immutable idx, ref BookText.Image img; book.images) {
293 if (iid == img.id) {
294 //writeln("image '", img.id, "' found");
295 lay.putObject(new BookImage(book, cast(uint)idx));
296 return;
301 void putParaContentInternal (Tag ct, int boldc=0, int italicc=0, int underc=0) {
302 if (ct is null) return;
303 auto c = lay.fontStyle.color;
304 scope(exit) lay.fontStyle.color = c;
305 saveTagId(ct);
306 switch (ct.name) {
307 case "strong": ++boldc; lay.fontStyle.bold = true; break;
308 case "emphasis": ++italicc; lay.fontStyle.italic = true; break;
309 case "image": putImage(ct); return;
310 case "style": return;
311 case "a":
312 if (ct.href.length) {
313 import std.conv : to;
314 //writeln("href found: '", ct.href, "' (", lay.nextWordIndex, ")");
315 meta.hrefs[lay.nextWordIndex] = BookMetadata.Anc(ct.href.to!dstring, lay.nextWordIndex);
316 ++underc;
317 lay.fontStyle.href = true;
318 lay.fontStyle.underline = true;
319 lay.fontStyle.color = colorTextHref.asUint;
321 break;
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 void putTagContents (Tag ct) {
337 if (ct is null) return;
338 saveTagId(ct);
339 foreach (Tag tag; ct.children) putParaContentInternal(tag);
340 lay.endPara();
343 void putParas (Tag ct) {
344 saveTagId(ct);
345 foreach (Tag tag; ct.children) {
346 if (tag.name.length == 0) continue;
347 if (tag.name == "p") putTagContents(tag);
348 else if (tag.name == "empty-line") lay.endLine();
349 else if (tag.name == "text-author") {}
350 else badTag(tag);
354 void putAuthor (Tag ct) {
355 saveTagId(ct);
356 foreach (Tag tag; ct.children) {
357 if (tag.name.length == 0) continue;
358 if (tag.name == "text-author") {
359 putTagContents(tag);
360 lay.endPara();
365 void putCite (Tag ct) {
366 lay.pushStyles();
367 scope(exit) lay.popStyles;
368 setCiteStyle(lay);
369 putParas(ct);
370 putAuthor(ct);
373 void putEpigraph (Tag ct) {
374 lay.pushStyles();
375 scope(exit) lay.popStyles;
376 setEpigraphStyle(lay);
377 putParas(ct);
378 putAuthor(ct);
381 void putStanza (Tag ct) {
382 saveTagId(ct);
383 foreach (Tag tag; ct.children) {
384 if (tag.name.length == 0) continue;
385 if (tag.name == "text-author") continue;
386 if (tag.name == "title") badTag(tag, "titles in stanzas are not supported yet");
387 if (tag.name == "subtitle") badTag(tag, "subtitles in stanzas are not supported yet");
388 if (tag.name == "epigraph") badTag(tag, "epigraphs in poems are not supported yet");
389 if (tag.name == "date") continue;
390 if (tag.name == "v") { putTagContents(tag); continue; }
391 badTag(tag);
395 void putPoem (Tag ct) {
396 saveTagId(ct);
397 lay.pushStyles();
398 scope(exit) lay.popStyles;
399 setPoemStyle(lay);
400 // put epigraph (not yet)
401 // put title and subtitle
402 foreach (Tag tag; ct.children) {
403 if (tag.name == "title") {
404 lay.pushStyles();
405 scope(exit) lay.popStyles;
406 putParas(tag);
407 lay.endPara(); // space
408 } else if (tag.name == "subtitle") {
409 lay.pushStyles();
410 scope(exit) lay.popStyles;
411 putParas(tag);
412 //lay.endPara(); // space
415 lay.endPara;
416 foreach (Tag tag; ct.children) {
417 if (tag.name.length == 0) continue;
418 if (tag.name == "text-author") continue;
419 if (tag.name == "title") continue;
420 if (tag.name == "subtitle") continue;
421 if (tag.name == "epigraph") badTag(tag, "epigraphs in poems are not supported yet");
422 if (tag.name == "date") continue;
423 if (tag.name == "stanza") { putStanza(tag); lay.put(LayText.EndParaCh); continue; }
424 badTag(tag);
426 putAuthor(ct);
429 void putSection (Tag sc) {
430 bool sectionRegistered = false;
432 void registerSection (Tag tag) {
433 import std.conv : to;
434 if (sectionRegistered) return;
435 sectionRegistered = true;
436 string text = tag.textContent.xstrip;
437 //lay.sections ~= lay.curWordIndex;
438 string name;
439 while (text.length) {
440 char ch = text.ptr[0];
441 text = text[1..$];
442 if (ch <= ' ' || ch == 127) {
443 if (name.length == 0) continue;
444 if (ch != '\n') ch = ' ';
445 if (ch == '\n') {
446 if (name[$-1] > ' ') name ~= "\n...";
447 text = text.xstrip;
448 } else {
449 if (name[$-1] > ' ') name ~= ' ';
451 } else {
452 name ~= ch;
455 name = xstrip(name);
456 if (name.length == 0) name = "* * *";
457 meta.sections ~= BookMetadata.Anc(name.to!dstring, lay.nextWordIndex);
460 saveTagId(sc);
461 foreach (Tag tag; sc.children) {
462 if (tag.name.length == 0) continue;
463 if (tag.name == "title") {
464 lay.pushStyles();
465 scope(exit) lay.popStyles;
466 setTitleStyle(lay);
467 registerSection(tag);
468 putParas(tag);
469 } else if (tag.name == "subtitle") {
470 lay.pushStyles();
471 scope(exit) lay.popStyles;
472 setSubtitleStyle(lay);
473 registerSection(tag);
474 putParas(tag);
475 } else if (tag.name == "epigraph") {
476 lay.pushStyles();
477 scope(exit) lay.popStyles;
478 setEpigraphStyle(lay);
479 putEpigraph(tag);
480 } else if (tag.name == "section") {
481 lay.pushStyles();
482 scope(exit) lay.popStyles;
483 setNormalStyle(lay);
484 putSection(tag);
485 } else if (tag.name == "p") {
486 putTagContents(tag);
487 } else if (tag.name == "image") {
488 lay.pushStyles();
489 scope(exit) lay.popStyles;
490 lay.lineStyle.leftpad = 0;
491 lay.lineStyle.rightpad = 0;
492 lay.lineStyle.paraIndent = 0;
493 lay.lineStyle.setCenter;
494 putImage(tag);
495 lay.endPara();
496 } else if (tag.name == "empty-line") {
497 lay.endPara();
498 } else if (tag.name == "cite") {
499 putCite(tag);
500 } else if (tag.name == "table") {
501 lay.pushStyles();
502 scope(exit) lay.popStyles;
503 lay.fontStyle.fontsize += 8;
504 lay.lineStyle.setCenter;
505 lay.put("TABLE SKIPPED");
506 lay.endPara();
507 } else if (tag.name == "poem") {
508 putPoem(tag);
509 } else if (tag.name == "image") {
510 } else if (tag.name == "style") {
511 } else {
512 badTag(tag);
515 lay.endPara;
518 foreach (Tag tag; book.content.children) {
519 if (tag.name == "title") {
520 lay.pushStyles();
521 scope(exit) lay.popStyles;
522 setTitleStyle(lay);
523 lay.endPara();
524 putParas(tag);
525 lay.endPara();
526 } else if (tag.name == "subtitle") {
527 lay.pushStyles();
528 scope(exit) lay.popStyles;
529 setTitleStyle(lay);
530 putParas(tag);
531 lay.endPara();
532 } else if (tag.name == "epigraph") {
533 putEpigraph(tag);
534 } else if (tag.name == "section") {
535 lay.pushStyles();
536 scope(exit) lay.popStyles;
537 setNormalStyle(lay);
538 putSection(tag);
539 } else if (tag.name == "image") {
540 putImage(tag);
544 lay.finalize();
546 return meta;
550 // ////////////////////////////////////////////////////////////////////////// //
551 private __gshared LayFontStash laf; // layouter font stash
554 // ////////////////////////////////////////////////////////////////////////// //
555 private void loadFmtFonts () {
556 laf = new LayFontStash();
558 laf.addFont("text", textFontName);
559 laf.addFont("texti", textiFontName);
560 laf.addFont("textb", textbFontName);
561 laf.addFont("textz", textzFontName);
563 laf.addFont("mono", monoFontName);
564 laf.addFont("monoi", monoiFontName);
565 laf.addFont("monob", monobFontName);
566 laf.addFont("monoz", monozFontName);
570 // ////////////////////////////////////////////////////////////////////////// //
571 // messages
572 struct ReformatWork {
573 shared(BookText) booktext;
574 string bookFileName;
575 int w, h;
578 struct ReformatWorkComplete {
579 int w, h;
580 shared(BookText) booktext;
581 shared(LayText) laytext;
582 shared(BookMetadata) meta;
585 struct QuitWork {
589 // ////////////////////////////////////////////////////////////////////////// //
590 void reformatThreadFn (Tid ownerTid) {
591 bool doQuit = false;
592 loadFmtFonts();
593 BookText book;
594 string newFileName;
595 int newW = -1, newH = -1;
596 while (!doQuit) {
597 book = null;
598 newFileName = null;
599 newW = newH = -1;
600 receive(
601 (ReformatWork w) {
602 //{ import std.stdio; writeln("reformat request received..."); }
603 book = cast(BookText)w.booktext;
604 newFileName = w.bookFileName;
605 newW = w.w;
606 newH = w.h;
607 if (newW < 1) newW = 1;
608 if (newH < 1) newH = 1;
610 (QuitWork w) {
611 doQuit = true;
614 if (!doQuit && newW > 0 && newH > 0) {
615 try {
616 if (book is null) {
617 //writeln("loading new book: '", newFileName, "'");
618 book = loadBook(newFileName);
621 int maxWidth = newW-4-2-BND_SCROLLBAR_WIDTH-2;
622 if (maxWidth < 64) maxWidth = 64;
624 // layout text
625 //writeln("layouting...");
626 auto stt = MonoTime.currTime;
627 auto lay = new LayText(laf, maxWidth);
628 lay.fontStyle.color = colorText.asUint;
630 auto meta = book.formatBook(lay);
632 auto ett = MonoTime.currTime-stt;
633 writeln("layouted in ", ett.total!"msecs", " milliseconds");
634 auto res = ReformatWorkComplete(newW, newH, cast(shared)book, cast(shared)lay, cast(shared)meta);
635 send(ownerTid, res);
636 } catch (Throwable e) {
637 // here, we are dead and fucked (the exact order doesn't matter)
638 import core.stdc.stdlib : abort;
639 import core.stdc.stdio : fprintf, stderr;
640 import core.memory : GC;
641 import core.thread : thread_suspendAll;
642 GC.disable(); // yeah
643 thread_suspendAll(); // stop right here, you criminal scum!
644 auto s = e.toString();
645 fprintf(stderr, "\n=== FATAL ===\n%.*s\n", cast(uint)s.length, s.ptr);
646 abort(); // die, you bitch!
650 send(ownerTid, QuitWork());