more fixes for fb2 idiocy
[xreader.git] / xreaderfmt.d
blobe4d5232f895b3cfab4d39606470af5d88e242a47
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.nanovg;
30 import iv.nanovg.oui.blendish;
31 import iv.vfs;
32 import iv.vfs.io;
34 import booktext;
36 import xreadercfg;
37 import xlayouter;
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"); }
55 string[] lines;
56 string[] comments;
57 try {
58 foreach (string line; VFile(buildPath(RcDir, ".lastfile")).byLineCopy) {
59 if (line != fname) {
60 lines ~= line;
61 } else {
62 while (lines.length && lines[$-1].isComment) {
63 comments ~= lines[$-1];
64 lines = lines[0..$-1];
68 } catch (Exception) {}
69 try { import std.file; mkdirRecurse(RcDir); } catch (Exception) {}
70 auto fo = VFile(buildPath(RcDir, ".lastfile"), "w");
71 foreach (string s; lines) fo.writeln(s);
72 foreach_reverse (string s; comments) fo.writeln(s);
73 fo.writeln(fname);
75 stateFileName = buildPath(RcDir, fname.baseName~".rc");
77 if (!eliteShipsLoaded) {
78 loadEliteShips();
79 eliteShipsLoaded = true;
82 return book;
86 // ////////////////////////////////////////////////////////////////////////// //
87 private struct ImageInfo {
88 enum MaxIdleFrames = 5;
89 int iid;
90 TrueColorImage img;
91 int framesNotUsed;
94 private __gshared ImageInfo[] nvgImageIds;
97 void releaseImages (NVGContext vg) {
98 if (nvgImageIds.length) {
99 foreach (ref ImageInfo nfo; nvgImageIds) {
100 if (nfo.iid >= 0) {
101 if (++nfo.framesNotUsed > ImageInfo.MaxIdleFrames) {
102 //conwriteln("freeing image with id ", nfo.iid);
103 vg.deleteImage(nfo.iid);
104 nfo.iid = -1;
105 nfo.img = null;
113 private int registerImage (NVGContext vg, TrueColorImage img) {
114 if (vg is null || img is null) return -1;
115 uint freeIdx = uint.max;
116 foreach (immutable idx, ref ImageInfo nfo; nvgImageIds) {
117 if (nfo.img is img) {
118 assert(nfo.iid >= 0);
119 nfo.framesNotUsed = 0;
120 return nfo.iid;
122 if (freeIdx != uint.max && nfo.iid < 0) freeIdx = cast(uint)idx;
124 int iid = vg.createImageFromMemoryImage(img); //vg.createImageRGBA(img.width, img.height, img.imageData.bytes[], NVGImageFlags.None);
125 if (iid < 0) return -1;
126 nvgImageIds ~= ImageInfo(iid, img, 0);
127 return iid;
131 class BookImage : LayObject {
132 BookText book;
133 uint imageidx;
134 TrueColorImage img;
135 this (BookText abook, uint aidx) { book = abook; imageidx = aidx; img = book.images[aidx].img; }
136 override int width () { return img.width; }
137 override int spacewidth () { return 0; }
138 override int height () { return img.height; }
139 override int ascent () { return img.height; }
140 override int descent () { return 0; }
141 override bool canbreak () { return true; }
142 override bool spaced () { return false; }
143 override void draw (NVGContext vg, float x, float y) {
144 y -= img.height;
145 vg.save();
146 scope(exit) vg.restore();
147 vg.beginPath();
148 int iid = vg.registerImage(img);
149 if (iid < 0) return;
150 vg.fillPaint(vg.imagePattern(x, y, img.width, img.height, 0, iid));
151 //vg.globalAlpha(0.5);
152 vg.rect(x, y, img.width, img.height);
153 vg.fill();
158 // ////////////////////////////////////////////////////////////////////////// //
159 class BookMetadata {
160 static struct Anc {
161 dstring name;
162 uint wordidx; // first word
164 Anc[] sections;
165 Anc[uint] hrefs; // `name` is dest
166 Anc[] ids;
170 // ////////////////////////////////////////////////////////////////////////// //
171 private void fixFont (LayText lay) {
172 if (lay.fontStyle.italic) {
173 if (lay.fontStyle.bold) lay.fontStyle.fontface = lay.fontFaceId("textz");
174 else lay.fontStyle.fontface = lay.fontFaceId("texti");
175 } else if (lay.fontStyle.bold) {
176 if (lay.fontStyle.italic) lay.fontStyle.fontface = lay.fontFaceId("textz");
177 else lay.fontStyle.fontface = lay.fontFaceId("textb");
178 } else {
179 lay.fontStyle.fontface = lay.fontFaceId("text");
183 private void setNormalStyle (LayText lay) {
184 lay.fontStyle.resetAttrs;
185 lay.fontStyle.fontsize = fsizeText;
186 lay.fontStyle.color = colorText.asUint;
187 lay.lineStyle.leftpad = 0;
188 lay.lineStyle.rightpad = 0;
189 lay.lineStyle.paraIndent = 3;
190 if (optJustify) lay.lineStyle.setJustify; else lay.lineStyle.setLeft;
191 fixFont(lay);
194 private void setTitleStyle (LayText lay) {
195 lay.fontStyle.resetAttrs;
196 lay.fontStyle.bold = true;
197 lay.fontStyle.fontsize = fsizeText+4;
198 lay.fontStyle.color = colorText.asUint;
199 lay.lineStyle.leftpad = 0;
200 lay.lineStyle.rightpad = 0;
201 lay.lineStyle.paraIndent = 0;
202 lay.lineStyle.setCenter;
203 fixFont(lay);
206 private void setSubtitleStyle (LayText lay) {
207 lay.fontStyle.resetAttrs;
208 lay.fontStyle.bold = true;
209 lay.fontStyle.fontsize = fsizeText+2;
210 lay.fontStyle.color = colorText.asUint;
211 lay.lineStyle.leftpad = 0;
212 lay.lineStyle.rightpad = 0;
213 lay.lineStyle.paraIndent = 0;
214 lay.lineStyle.setCenter;
215 fixFont(lay);
218 private void setCiteStyle (LayText lay) {
219 lay.fontStyle.resetAttrs;
220 lay.fontStyle.italic = true;
221 lay.fontStyle.fontsize = fsizeText;
222 lay.fontStyle.color = colorText.asUint;
223 lay.lineStyle.leftpad = fsizeText*3;
224 lay.lineStyle.rightpad = fsizeText*3;
225 lay.lineStyle.paraIndent = 3;
226 if (optJustify) lay.lineStyle.setJustify; else lay.lineStyle.setLeft;
227 fixFont(lay);
230 private void setEpigraphStyle (LayText lay) {
231 lay.fontStyle.resetAttrs;
232 lay.fontStyle.italic = true;
233 lay.fontStyle.fontsize = fsizeText;
234 lay.fontStyle.color = colorText.asUint;
235 lay.lineStyle.leftpad = lay.width/3;
236 lay.lineStyle.rightpad = 0;
237 lay.lineStyle.paraIndent = 0;
238 lay.lineStyle.setRight;
239 fixFont(lay);
242 private void setPoemStyle (LayText lay) {
243 lay.fontStyle.resetAttrs;
244 lay.fontStyle.italic = true;
245 lay.fontStyle.fontsize = fsizeText;
246 lay.fontStyle.color = colorText.asUint;
247 lay.lineStyle.leftpad = fsizeText*3;
248 lay.lineStyle.rightpad = 0;
249 lay.lineStyle.paraIndent = 0;
250 lay.lineStyle.setLeft;
251 fixFont(lay);
255 // ////////////////////////////////////////////////////////////////////////// //
256 public BookMetadata formatBook (BookText book, LayText lay) {
257 assert(book !is null);
259 BookMetadata meta = new BookMetadata();
261 void dumpTree (Tag ct) {
262 while (ct !is null) {
263 conwriteln("tag: ", ct.name);
264 ct = ct.parent;
268 void badTag (Tag tag, string msg=null) {
269 import std.string : format, indexOf;
270 assert(tag !is null);
271 dumpTree(tag);
272 if (msg.length == 0) msg = "invalid tag: '%s'";
273 if (msg.indexOf("%s") >= 0) {
274 throw new Exception(msg.format(tag.name));
275 } else {
276 throw new Exception(msg);
280 void saveTagId (Tag tag) {
281 if (tag.id.length) {
282 import std.conv : to;
283 meta.ids ~= BookMetadata.Anc(tag.id.to!dstring, lay.nextWordIndex);
287 void putImage (Tag tag) {
288 if (tag is null || tag.href.length < 2 || tag.href[0] != '#') return;
289 string iid = tag.href[1..$];
290 //conwriteln("searching for image with href '", iid, "' (", book.images.length, ")");
291 foreach (immutable idx, ref BookText.Image img; book.images) {
292 if (iid == img.id) {
293 //conwriteln("image '", img.id, "' found");
294 lay.putObject(new BookImage(book, cast(uint)idx));
295 return;
300 void putParaContentInternal (Tag ct, int boldc=0, int italicc=0, int underc=0) {
301 if (ct is null) return;
302 auto c = lay.fontStyle.color;
303 scope(exit) lay.fontStyle.color = c;
304 saveTagId(ct);
305 switch (ct.name) {
306 case "strong": ++boldc; lay.fontStyle.bold = true; break;
307 case "emphasis": ++italicc; lay.fontStyle.italic = true; break;
308 case "image": putImage(ct); return;
309 case "style": return;
310 case "a":
311 if (ct.href.length) {
312 import std.conv : to;
313 //conwriteln("href found: '", ct.href, "' (", lay.nextWordIndex, ")");
314 meta.hrefs[lay.nextWordIndex] = BookMetadata.Anc(ct.href.to!dstring, lay.nextWordIndex);
315 ++underc;
316 lay.fontStyle.href = true;
317 lay.fontStyle.underline = true;
318 lay.fontStyle.color = colorTextHref.asUint;
320 break;
321 case "sub": break; // some idiotic files has this
322 case "sup": break; // some idiotic files has this
323 case "": lay.put(ct.text); return;
324 default: badTag(ct);
326 fixFont(lay);
327 foreach (Tag tag; ct.children) putParaContentInternal(tag, boldc, italicc, underc);
328 switch (ct.name) {
329 case "strong": if (--boldc == 0) lay.fontStyle.bold = false; break;
330 case "emphasis": if (--italicc == 0) lay.fontStyle.italic = false; break;
331 case "a": if (--underc == 0) lay.fontStyle.underline = false; lay.fontStyle.href = false; break;
332 default:
334 fixFont(lay);
337 bool onlyImagePara (Tag ct) {
338 if (ct is null) return false;
339 int count;
340 foreach (Tag tag; ct.children) {
341 if (tag.name.length == 0) {
342 // text
343 if (tag.text.xstrip.length != 0) return false;
344 continue;
346 if (count != 0 || tag.name != "image") return false;
347 ++count;
349 return (count == 1);
352 void putTagContents(bool xdump=false) (Tag ct, int boldc=0, int italicc=0, int underc=0) {
353 if (ct is null) return;
354 saveTagId(ct);
355 if (onlyImagePara(ct)) {
356 lay.pushStyles();
357 scope(exit) lay.popStyles;
358 lay.lineStyle.setCenter;
359 foreach (Tag tag; ct.children) putParaContentInternal(tag, boldc, italicc, underc);
360 } else {
361 //if (ct.name == "subtitle") conwriteln("text: [", ct.children[0].text, "]");
362 foreach (Tag tag; ct.children) {
363 static if (xdump) conwriteln("text: [", tag.text, "]");
364 putParaContentInternal(tag, boldc, italicc, underc);
367 lay.endPara();
370 void delegate (Tag ct) putPoemx;
371 void putParas (Tag ct) {
372 saveTagId(ct);
373 foreach (Tag tag; ct.children) {
374 if (tag.name.length == 0) continue;
375 if (tag.name == "p") putTagContents(tag);
376 else if (tag.name == "empty-line") lay.endLine();
377 else if (tag.name == "text-author") {}
378 else if (tag.name == "strong") putTagContents(tag, 1);
379 else if (tag.name == "poem") { putPoemx(tag); lay.put(LayText.EndParaCh); }
380 else if (tag.name == "subtitle") {
381 //FIXME:??? somehow this is invisible
382 lay.pushStyles();
383 scope(exit) lay.popStyles;
384 setSubtitleStyle(lay);
385 if (tag.text.length) { putParaContentInternal(tag); lay.endPara(); } else putTagContents(tag);
387 else if (tag.name == "sub" || tag.name == "sup") putTagContents(tag); // some idiotic files has this
388 else badTag(tag);
392 void putAuthor (Tag ct) {
393 saveTagId(ct);
394 foreach (Tag tag; ct.children) {
395 if (tag.name.length == 0) continue;
396 if (tag.name == "text-author") {
397 putTagContents(tag);
398 lay.endPara();
403 void putCite (Tag ct) {
404 lay.pushStyles();
405 scope(exit) lay.popStyles;
406 setCiteStyle(lay);
407 putParas(ct);
408 putAuthor(ct);
411 void putEpigraph (Tag ct) {
412 lay.pushStyles();
413 scope(exit) lay.popStyles;
414 setEpigraphStyle(lay);
415 putParas(ct);
416 putAuthor(ct);
419 void putStanza (Tag ct) {
420 saveTagId(ct);
421 foreach (Tag tag; ct.children) {
422 if (tag.name.length == 0) continue;
423 if (tag.name == "text-author") continue;
424 if (tag.name == "title") badTag(tag, "titles in stanzas are not supported yet");
425 if (tag.name == "subtitle") badTag(tag, "subtitles in stanzas are not supported yet");
426 if (tag.name == "epigraph") badTag(tag, "epigraphs in poems are not supported yet");
427 if (tag.name == "date") continue;
428 if (tag.name == "v") { putTagContents(tag); continue; }
429 badTag(tag);
433 void putPoem (Tag ct) {
434 saveTagId(ct);
435 lay.pushStyles();
436 scope(exit) lay.popStyles;
437 setPoemStyle(lay);
438 // put epigraph (not yet)
439 // put title and subtitle
440 foreach (Tag tag; ct.children) {
441 if (tag.name == "title") {
442 lay.pushStyles();
443 scope(exit) lay.popStyles;
444 putParas(tag);
445 lay.endPara(); // space
446 } else if (tag.name == "subtitle") {
447 lay.pushStyles();
448 scope(exit) lay.popStyles;
449 putParas(tag);
450 //lay.endPara(); // space
453 lay.endPara;
454 foreach (Tag tag; ct.children) {
455 if (tag.name.length == 0) continue;
456 if (tag.name == "text-author") continue;
457 if (tag.name == "title") continue;
458 if (tag.name == "subtitle") continue;
459 if (tag.name == "epigraph") badTag(tag, "epigraphs in poems are not supported yet");
460 if (tag.name == "date") continue;
461 if (tag.name == "stanza") { putStanza(tag); lay.put(LayText.EndParaCh); continue; }
462 badTag(tag);
464 putAuthor(ct);
466 putPoemx = &putPoem;
468 void putSection (Tag sc) {
469 bool sectionRegistered = false;
471 void registerSection (Tag tag) {
472 import std.conv : to;
473 if (sectionRegistered) return;
474 sectionRegistered = true;
475 string text = tag.textContent.xstrip;
476 //lay.sections ~= lay.curWordIndex;
477 string name;
478 while (text.length) {
479 char ch = text.ptr[0];
480 text = text[1..$];
481 if (ch <= ' ' || ch == 127) {
482 if (name.length == 0) continue;
483 if (ch != '\n') ch = ' ';
484 if (ch == '\n') {
485 if (name[$-1] > ' ') name ~= "\n...";
486 text = text.xstrip;
487 } else {
488 if (name[$-1] > ' ') name ~= ' ';
490 } else {
491 name ~= ch;
494 name = xstrip(name);
495 if (name.length == 0) name = "* * *";
496 meta.sections ~= BookMetadata.Anc(name.to!dstring, lay.nextWordIndex);
499 saveTagId(sc);
500 foreach (Tag tag; sc.children) {
501 if (tag.name.length == 0) continue;
502 if (tag.name == "title") {
503 lay.pushStyles();
504 scope(exit) lay.popStyles;
505 setTitleStyle(lay);
506 registerSection(tag);
507 putParas(tag);
508 } else if (tag.name == "subtitle") {
509 lay.pushStyles();
510 scope(exit) lay.popStyles;
511 setSubtitleStyle(lay);
512 registerSection(tag);
513 putParas(tag);
514 } else if (tag.name == "epigraph") {
515 lay.pushStyles();
516 scope(exit) lay.popStyles;
517 setEpigraphStyle(lay);
518 putEpigraph(tag);
519 } else if (tag.name == "section") {
520 lay.pushStyles();
521 scope(exit) lay.popStyles;
522 setNormalStyle(lay);
523 putSection(tag);
524 } else if (tag.name == "p") {
525 putTagContents(tag);
526 } else if (tag.name == "image") {
527 lay.pushStyles();
528 scope(exit) lay.popStyles;
529 lay.lineStyle.leftpad = 0;
530 lay.lineStyle.rightpad = 0;
531 lay.lineStyle.paraIndent = 0;
532 lay.lineStyle.setCenter;
533 putImage(tag);
534 lay.endPara();
535 } else if (tag.name == "empty-line") {
536 lay.endPara();
537 } else if (tag.name == "cite") {
538 putCite(tag);
539 } else if (tag.name == "table") {
540 lay.pushStyles();
541 scope(exit) lay.popStyles;
542 lay.fontStyle.fontsize += 8;
543 lay.lineStyle.setCenter;
544 lay.put("TABLE SKIPPED");
545 lay.endPara();
546 } else if (tag.name == "poem") {
547 putPoem(tag);
548 } else if (tag.name == "image") {
549 } else if (tag.name == "style") {
550 } else {
551 badTag(tag);
554 lay.endPara;
557 foreach (Tag tag; book.content.children) {
558 if (tag.name == "title") {
559 lay.pushStyles();
560 scope(exit) lay.popStyles;
561 setTitleStyle(lay);
562 lay.endPara();
563 putParas(tag);
564 lay.endPara();
565 } else if (tag.name == "subtitle") {
566 lay.pushStyles();
567 scope(exit) lay.popStyles;
568 setTitleStyle(lay);
569 putParas(tag);
570 lay.endPara();
571 } else if (tag.name == "epigraph") {
572 putEpigraph(tag);
573 } else if (tag.name == "section") {
574 lay.pushStyles();
575 scope(exit) lay.popStyles;
576 setNormalStyle(lay);
577 putSection(tag);
578 } else if (tag.name == "image") {
579 putImage(tag);
583 lay.finalize();
585 return meta;
589 // ////////////////////////////////////////////////////////////////////////// //
590 private __gshared LayFontStash laf; // layouter font stash
593 // ////////////////////////////////////////////////////////////////////////// //
594 private void loadFmtFonts () {
595 laf = new LayFontStash();
597 laf.addFont("text", textFontName);
598 laf.addFont("texti", textiFontName);
599 laf.addFont("textb", textbFontName);
600 laf.addFont("textz", textzFontName);
602 laf.addFont("mono", monoFontName);
603 laf.addFont("monoi", monoiFontName);
604 laf.addFont("monob", monobFontName);
605 laf.addFont("monoz", monozFontName);
609 // ////////////////////////////////////////////////////////////////////////// //
610 // messages
611 struct ReformatWork {
612 shared(BookText) booktext;
613 string bookFileName;
614 int w, h;
617 struct ReformatWorkComplete {
618 int w, h;
619 shared(BookText) booktext;
620 shared(LayText) laytext;
621 shared(BookMetadata) meta;
624 struct QuitWork {
628 // ////////////////////////////////////////////////////////////////////////// //
629 void reformatThreadFn (Tid ownerTid) {
630 bool doQuit = false;
631 loadFmtFonts();
632 BookText book;
633 string newFileName;
634 int newW = -1, newH = -1;
635 while (!doQuit) {
636 book = null;
637 newFileName = null;
638 newW = newH = -1;
639 receive(
640 (ReformatWork w) {
641 //{ conwriteln("reformat request received..."); }
642 book = cast(BookText)w.booktext;
643 newFileName = w.bookFileName;
644 newW = w.w;
645 newH = w.h;
646 if (newW < 1) newW = 1;
647 if (newH < 1) newH = 1;
649 (QuitWork w) {
650 doQuit = true;
653 if (!doQuit && newW > 0 && newH > 0) {
654 try {
655 if (book is null) {
656 //conwriteln("loading new book: '", newFileName, "'");
657 book = loadBook(newFileName);
660 int maxWidth = newW-4-2-BND_SCROLLBAR_WIDTH-2;
661 if (maxWidth < 64) maxWidth = 64;
663 // layout text
664 //conwriteln("layouting...");
665 auto stt = MonoTime.currTime;
666 auto lay = new LayText(laf, maxWidth);
667 lay.fontStyle.color = colorText.asUint;
669 auto meta = book.formatBook(lay);
671 auto ett = MonoTime.currTime-stt;
672 conwriteln("layouted in ", ett.total!"msecs", " milliseconds");
673 auto res = ReformatWorkComplete(newW, newH, cast(shared)book, cast(shared)lay, cast(shared)meta);
674 send(ownerTid, res);
675 } catch (Throwable e) {
676 // here, we are dead and fucked (the exact order doesn't matter)
677 import core.stdc.stdlib : abort;
678 import core.stdc.stdio : fprintf, stderr;
679 import core.memory : GC;
680 import core.thread : thread_suspendAll;
681 GC.disable(); // yeah
682 thread_suspendAll(); // stop right here, you criminal scum!
683 auto s = e.toString();
684 fprintf(stderr, "\n=== FATAL ===\n%.*s\n", cast(uint)s.length, s.ptr);
685 abort(); // die, you bitch!
689 send(ownerTid, QuitWork());