]> git.kianting.info Git - clo/blob - src/libclo/index.ts
f48f448d9bcabebaeaba7d0b21a60923bf4baf8b
[clo] / src / libclo / index.ts
1 import {tkTree} from "../parser";
2 import {FontStyle, TextStyle, TextWeight, fontStyleTofont, fontPathPSNamePair} from "../canva";
3 import * as fontkit from "fontkit";
4 import * as breakLines from "./breakLines";
5 const PDFDocument = require('pdfkit');
6 import * as fs from "fs";
7 import { Style } from "util";
8 import { time } from "console";
9
10
11 /**
12 * TYPES
13 */
14
15 /**
16 * text direction
17 * LTR - left to right
18 * TTB - top to bottom
19 * etc.
20 */
21 export enum Direction{
22 LTR,
23 RTL,
24 TTB,
25 BTT,
26 }
27
28 /**
29 * Horizonal glue.
30 * - stretchFactor : the stretch factor in float
31 */
32 export interface HGlue{
33 stretchFactor: number
34 }
35
36 export interface BreakPoint{
37 original : BoxesItem,
38 newLined : BoxesItem
39 }
40
41 export type BoxesItem = HGlue | Box | BreakPoint | BoxesItem[] ;
42
43 /**
44 * frame box is a subclass of box
45 * - directionInsideLine : text direction inside a line
46 * - baselineskip : the distance between baselines in px
47 */
48 export interface FrameBox extends Box{
49 directionInsideLine : Direction,
50 baseLineskip : number | null,
51 }
52
53 export interface CharBox extends Box{
54 minX: number,
55 maxX: number,
56 minY: number,
57 maxY: number,
58
59 }
60
61 /**
62 * a basic Box
63 * - x : pt
64 * - y : pt
65 * - textStyle :
66 * - direction :
67 * - width : x_advance pt
68 * - content :
69 */
70 export interface Box{
71 x : number | null,
72 y : number | null,
73 textStyle : TextStyle | null,
74 direction : Direction,
75 width : number,
76 height : number,
77 content : string | Box[] | null,
78 }
79
80
81 /**
82 * DEFAULT CONST PART
83 */
84 export const A4_IN_PX = {"width" : 793.7,
85 "height" : 1122.5};
86
87 export const defaultTextStyle : TextStyle = {
88 family : "FreeSerif",
89 size : ptToPx(12),
90 textWeight : TextWeight.REGULAR,
91 fontStyle : FontStyle.ITALIC,
92 }
93
94 export const defaultFrameStyle : FrameBox = {
95 directionInsideLine : Direction.LTR,
96 direction : Direction.TTB,
97 baseLineskip : ptToPx(15),
98 textStyle : defaultTextStyle,
99 x : A4_IN_PX.width * 0.10 ,
100 y : A4_IN_PX.height * 0.10 ,
101 width : A4_IN_PX.width * 0.80 ,
102 height : A4_IN_PX.height * 0.80 ,
103 content : null,
104 };
105
106 /**
107 * definition for cjk scripts
108 * - Hani : Han Character
109 * - Hang : Hangul
110 * - Bopo : Bopomofo
111 * - Kana : Katakana
112 * - Hira : Hiragana
113 */
114 export const cjkvBlocksInRegex = ["Hani", "Hang", "Bopo", "Kana", "Hira"];
115
116 export const cjkvRegexPattern = new RegExp("((?:" +
117 cjkvBlocksInRegex.map((x)=>"\\p{Script_Extensions="+x+"}").join("|") + ")+)", "gu");
118 /**
119 * FUNCTION PART
120 */
121 /**
122 * convert from ptToPx
123 * @param pt pt size value
124 * @returns the corresponding px value
125 */
126 export function ptToPx(pt : number) : number{
127 return pt * 4.0 / 3.0;
128 }
129
130
131
132 /**
133 * REGISTER PART
134 */
135
136 /**
137 * convert '\n\n' to newline command ["nl"]
138 * @param arr the input `tkTree`
139 * @param clo the `Clo` object
140 * @returns the input tktree
141 */
142 export function twoReturnsToNewline(arr : tkTree, clo : Clo): tkTree{
143 var middle : tkTree = [];
144
145 for (let i = 0; i < arr.length; i++) {
146 var item = arr[i];
147 if (!Array.isArray(item)){
148 middle = middle.concat(item.split(/(\n\n)/g));
149 }
150 else{
151 middle.push(item);
152 }
153 }
154
155 var result : tkTree = [];
156 for (let j = 0; j < middle.length; j++){
157 var item = middle[j];
158 if (!Array.isArray(item) && item == "\n\n"){
159 result.push(["nl"]); // push a newline command to the result `tkTree`
160 }
161 else{
162 result.push(middle[j]);
163 }
164 }
165
166 return result;
167 }
168
169 /**
170 * split CJKV and non-CJKV
171 *
172 * @param arr : input tkTree
173 * @returns a splitted tkTree (by CJK and NonCJK)
174 * - Examples:
175 * ```
176 * [`many臺中daylight`] => [`many`, `臺中`, `dahylight`]
177 * ```
178 */
179 export function splitCJKV(arr : tkTree, clo : Clo): tkTree{
180 var result : tkTree = [];
181 for (let i = 0; i < arr.length; i++) {
182 var item = arr[i];
183 if (!Array.isArray(item)){
184 result = result.concat(item.split(cjkvRegexPattern));
185 }
186 else{
187 result.push(item);
188 }
189 }
190
191 return result;
192 }
193
194 /**
195 * hyphenation for a clo document
196 * @param arr the array for a `tkTree`
197 * @param clo the Clo object
198 */
199 export function hyphenForClo(arr : tkTree, clo : Clo): tkTree{
200 let hyphenLanguage : string = clo.attrs["hyphenLanguage"];
201 let res = hyphenTkTree(arr, hyphenLanguage);
202 return res;
203
204 }
205
206 /**
207 * convert spaces to Breakpoint
208 * \s+ => ["bp" [\s+] ""]
209 * @param arr the tkTree input text stream
210 * @param clo the Clo object
211 * @returns the converted object
212 */
213 export function spacesToBreakpoint(arr : tkTree, clo : Clo) : tkTree{
214 let spacePattern = /^([ \t]+)$/g;
215 var result : tkTree = [];
216 for (let i = 0; i < arr.length; i++){
217 var item = arr[i];
218 if (!Array.isArray(item) && item.match(spacePattern)){
219 // push a breakpoint command to the result `tkTree`
220 result.push([ 'bp', [["hglue", "0.1"], item] , "" ]);
221 }
222 else{
223 result.push(item);
224 }
225 }
226
227 return result;
228 }
229
230 /**
231 * remove all the `` (empty string) in the arr
232 * @param arr the tkTree to be filtered
233 * @param clo the Clo file
234 */
235 export function filterEmptyString(arr : tkTree, clo : Clo) : tkTree{
236 if (Array.isArray(arr)){
237 arr.filter((x)=>{return x != ``;});
238 }
239
240 return arr;
241 }
242
243
244 /**
245 * OTHER FUNCTIONS
246 */
247
248 /**
249 * hyphenate for a tkTree
250 * - hyphenation => ["bp", "", "-"]
251 * @param arr the tkTree array
252 * @param lang ISO 639 code for the language
253 */
254 export function hyphenTkTree(arr : tkTree, lang: string) : tkTree{
255 // import corresponding hyphen language data and function
256 let hyphen = require("hyphen/"+lang);
257
258 let result :tkTree[] = [];
259 for (let i = 0; i < arr.length; i++) {
260 let element = arr[i];
261 let splitter = "分"; // a CJKV
262 if (!Array.isArray(element)){
263 let hyphenatedElement : string = hyphen.hyphenateSync(element, {hyphenChar :splitter});
264 let hyphenatedSplitted : tkTree = hyphenatedElement.split(splitter);
265 var newSplitted : tkTree = [];
266 for (var j=0; j<hyphenatedSplitted.length-1;j++){
267 newSplitted.push(hyphenatedSplitted[j]);
268 // "bp" for breakpoint
269 newSplitted.push(["bp", "", "-"]); //insert a breakable point (bp) mark
270 }
271 newSplitted.push(hyphenatedSplitted[hyphenatedSplitted.length-1]);
272
273 result = result.concat(newSplitted);
274
275 }else{
276 result.push(element);
277 }
278
279 }
280
281 return result;
282 }
283
284
285
286 /**
287 * calculate the text width and Height with a given `TextStyle`
288 * @param preprocessed
289 * @param defaultFontStyle
290 */
291 export async function calculateTextWidthHeight(element : tkTree, style : TextStyle): Promise<BoxesItem[]> {
292 var res = [];
293 var styleCache = {};
294 var fontCache = {};
295
296 for (var i=0; i<element.length; i++){
297 let item = await calculateTextWidthHeightAux(element[i], style, <TextStyle>styleCache, <fontkit.Font>fontCache);
298 styleCache = item[1];
299 fontCache = item[2];
300 res.push(item[0]);
301 }
302
303 res = res.flat();
304
305 return res;
306 }
307
308
309 /**
310 * calculate the text width and Height with a given `TextStyle`
311 * @param preprocessed
312 * @param defaultFontStyle
313 */
314 export async function calculateTextWidthHeightAux(element : tkTree,
315 style : TextStyle,
316 styleCache : TextStyle,
317 fontCache : fontkit.Font): Promise<[BoxesItem, TextStyle, fontkit.Font] > {
318 var result : BoxesItem = [];
319 var font;
320
321 if (style === styleCache){
322 font = fontCache;
323 }else {
324
325
326 let fontPair = fontStyleTofont(style);
327
328 if (fontPair.path.match(/\.ttc$/)){
329 font = await fontkit.openSync(fontPair.path, fontPair.psName);
330 styleCache = style;
331 fontCache = font;
332
333 }
334 else{
335 font = await fontkit.openSync(fontPair.path);
336 styleCache = style;
337 fontCache = font;
338 }
339
340
341
342 }
343
344
345 if (!Array.isArray(element)){
346 var run = font.layout(element, undefined, undefined, undefined, "ltr");
347
348
349
350 for (var j=0;j<run.glyphs.length;j++){
351 let runGlyphsItem = run.glyphs[j];
352
353
354 let item : CharBox = {
355 x : null,
356 y : null,
357 textStyle : style,
358 direction : Direction.LTR,
359 width : (runGlyphsItem.advanceWidth)*(style.size)/1000 * 0.75, // in pt
360 height : (runGlyphsItem.bbox.maxY - runGlyphsItem.bbox.minY)*(style.size)/1000 * 0.75, // in pt
361 content : element[j],
362 minX : runGlyphsItem.bbox.minX,
363 maxX : runGlyphsItem.bbox.maxX,
364 minY : runGlyphsItem.bbox.minY,
365 maxY : runGlyphsItem.bbox.maxY
366 }
367
368 result.push(item);
369
370 }
371 return [result, styleCache, fontCache];
372
373
374
375
376 }else if(element[0] == "bp"){
377
378
379 var beforeNewLine = (await calculateTextWidthHeightAux(element[1], style, styleCache, fontCache))[0];
380 if (Array.isArray(beforeNewLine)){
381 beforeNewLine = beforeNewLine.flat();
382 }
383
384 let afterNewLine = (await calculateTextWidthHeightAux(element[2], style, styleCache, fontCache))[0];
385 if (Array.isArray(afterNewLine)){
386 afterNewLine = afterNewLine.flat();
387 }
388
389 let breakPointNode : BreakPoint = {
390 original : beforeNewLine,
391 newLined : afterNewLine,
392 }
393
394 return [breakPointNode, styleCache, fontCache];
395 }else if(element[0] == "hglue" && !Array.isArray(element[1])){
396 let hGlue : HGlue = {stretchFactor : parseFloat(element[1])}
397 return [hGlue, styleCache, fontCache];
398 }
399 else{
400 return [await calculateTextWidthHeight(element, style), styleCache, fontCache];
401 }
402 }
403
404
405
406
407 /**
408 * whole document-representing class
409 */
410 export class Clo{
411 /** storing the text string into the main frame */
412 mainStream : Array<string>;
413 /** array of preprocessor functions to preprocess the `mainStream` */
414 preprocessors : Array<Function>;
415 /** the attributes for the Clo */
416 attrs: {[index: string]:any} ; // a4 size(x,y)
417
418
419 constructor(){
420 this.preprocessors = [];
421 this.mainStream = [];
422 this.attrs = {
423 "page" : A4_IN_PX, // default for a4. in px of [x, y]
424 "defaultFrameStyle" : defaultFrameStyle, // defaultFrameStyle
425 "hyphenLanguage" : 'en' // hyphenated in the language (in ISO 639)
426 };
427
428
429
430 // register the precessor functions
431 this.preprocessorRegister(splitCJKV);
432 this.preprocessorRegister(hyphenForClo);
433 this.preprocessorRegister(twoReturnsToNewline);
434 this.preprocessorRegister(spacesToBreakpoint);
435 this.preprocessorRegister(filterEmptyString);
436 }
437
438 public setAttr(attr : string, val : any):void{
439 Object.assign(this.attrs, attr, val);
440 }
441
442 public getAttr(attr:string) : any{
443 if (Object.keys(this.attrs).length === 0){
444 return this.attrs[attr];
445 }else{
446 return undefined;
447 }
448
449 }
450
451 /**
452 * register a function of preprocessor
453 * @param f a function
454 */
455 public preprocessorRegister(f : Function){
456 this.preprocessors.push(f);
457 }
458
459 public async generatePdf(){
460
461 // preprocessed
462 var preprocessed = this.mainStream;
463 for (var i = 0; i<this.preprocessors.length; i++){
464 preprocessed = this.preprocessors[i](preprocessed, this);
465 }
466
467 // generate the width and height of the stream
468
469 let defaultFontStyle : TextStyle = this.attrs.defaultFrameStyle.textStyle;
470
471
472 let a = await calculateTextWidthHeight(preprocessed, defaultFontStyle);
473
474 let breakLineAlgorithms = new breakLines.BreakLineAlgorithm();
475
476 let segmentedNodes = breakLineAlgorithms.segmentedNodes(a, this.attrs.defaultFrameStyle.width);
477
478 let segmentedNodesToBox =
479 this.segmentedNodesToFrameBox(segmentedNodes, <FrameBox>this.attrs.defaultFrameStyle);
480
481
482 let boxesFixed = this.fixenBoxesPosition(segmentedNodesToBox);
483
484
485
486
487 // generate pdf
488 const doc = new PDFDocument({size: 'A4'});
489 doc.pipe(fs.createWriteStream('output.pdf'));
490 this.grid(doc);
491
492 let styleCache : any = {};
493 let fontPairCache : fontPathPSNamePair = {path : "", psName : ""};
494 await this.putText(doc, boxesFixed, <TextStyle>styleCache, fontPairCache);
495 // putChar
496 doc.end();
497
498
499 }
500
501 async putText(doc : PDFKit.PDFDocument, box : Box, styleCache : TextStyle,
502 fontPairCache : fontPathPSNamePair):
503 Promise<[PDFKit.PDFDocument, TextStyle, fontPathPSNamePair]>{
504 var fontPair;
505
506
507 if (box.textStyle !== null){
508
509 if(box.textStyle == styleCache){
510 fontPair = fontPairCache;
511 }else{
512 fontPair = fontStyleTofont(box.textStyle);
513 styleCache = box.textStyle;
514 fontPairCache = fontPair;
515
516 if (fontPair.path.match(/\.ttc$/g)){
517 doc
518 .font(fontPair.path, fontPair.psName)
519 .fontSize(box.textStyle.size * 0.75);}
520 else{
521 doc
522 .font(fontPair.path)
523 .fontSize(box.textStyle.size * 0.75); // 0.75 must added!
524 }
525 }
526
527 if (box.textStyle.color !== undefined){
528 doc.fill(box.textStyle.color);
529 }
530
531 if (Array.isArray(box.content)){
532 for (var k=0; k<box.content.length; k++){
533
534 let tmp = await this.putText(doc, box.content[k], styleCache, fontPairCache);
535 doc = tmp[0];
536 styleCache = tmp[1];
537 fontPairCache = tmp[2];
538 }
539 }else if (box.content !== null){
540 await doc.text(box.content,
541 (box.x!==null? box.x: undefined),
542 (box.y!==null? box.y: undefined));
543 }
544
545 }
546
547
548 return [doc, styleCache, fontPairCache];
549 };
550
551
552
553 private grid(doc: any) {
554 for (var j = 0; j < A4_IN_PX.width; j += 5) {
555 if (j % 50 == 0) {
556 doc.save().fill('#000000')
557 .fontSize(8).text(j.toString(), j*0.75, 50);
558
559 doc
560 .save()
561 .lineWidth(0.4)
562 .strokeColor("#dddddd")
563 .moveTo(j*0.75, 0)
564 .lineTo(j*0.75, 1000)
565 .stroke();
566 }
567
568 doc
569 .save()
570 .lineWidth(0.2)
571 .strokeColor("#dddddd")
572 .moveTo(j*0.75, 0)
573 .lineTo(j*0.75, 1000)
574 .stroke();
575 }
576
577 for (var i = 0; i < 1050; i += 5) {
578 if (i % 50 == 0) {
579 doc.save()
580 .fontSize(8).text(i.toString(), 50, i*0.75);
581
582 doc
583 .save()
584 .lineWidth(0.4)
585 .strokeColor("#bbbbbb")
586 .moveTo(0, i*0.75)
587 .lineTo(1000, i*0.75)
588 .stroke();
589 }
590 doc
591 .save()
592 .lineWidth(0.2)
593 .strokeColor("#bbbbbb")
594 .moveTo(0, i*0.75)
595 .lineTo(1000, i*0.75)
596 .stroke();
597 }
598 doc
599 .save()
600 .moveTo(0, 200)
601 .lineTo(1000, 200)
602 .fill('#FF3300');
603 }
604
605 /**
606 * make all the nest boxes's position fixed
607 * @param box the main boxes
608 * @returns the fixed boxes
609 */
610 fixenBoxesPosition(box : Box) : Box{
611 var currX : number = (box.x!==null?box.x:0); // current x
612 var currY : number =(box.y!==null?box.y:0); // current y
613 if (Array.isArray(box.content)){
614 for (var i=0; i<box.content.length; i++){
615 if (box.direction == Direction.LTR){
616 box.content[i].x = currX;
617 box.content[i].y = currY;
618 let elementWidth = box.content[i].width;
619 if(elementWidth !== null){
620 currX += elementWidth;
621 }
622
623 }
624 if (box.direction == Direction.TTB){
625 box.content[i].x = currX;
626 box.content[i].y = currY;
627 let elementHeight = box.content[i].height;
628 if(elementHeight !== null){
629 currY += elementHeight;
630 }
631
632 }
633
634
635 box.content[i] = this.fixenBoxesPosition(box.content[i]);
636 }
637 }
638
639 return box;
640 }
641
642 /**
643 * input a `segmentedNodes` and a layed `frame`, return a big `Box` that nodes is put in.
644 * @param segmentedNodes the segmentnodes to be input
645 * @param frame the frame to be layed out.
646 * @returns the big `Box`.
647 */
648 segmentedNodesToFrameBox(segmentedNodes : BoxesItem[][], frame : FrameBox) : Box{
649 let baseLineskip = frame.baseLineskip;
650 let boxArrayEmpty : Box[] = [];
651 let bigBox : Box = {
652 x : (frame.x !==null? frame.x * 0.75 : null),
653 y : (frame.y !==null? frame.y * 0.75 : null),
654 textStyle : frame.textStyle,
655 direction : frame.direction,
656 width : frame.width,
657 height :frame.height,
658 content : boxArrayEmpty,
659 }
660
661 var bigBoxContent : Box[] = boxArrayEmpty;
662
663 let segmentedNodesFixed = segmentedNodes.map((x)=>this.removeBreakPoints
664 (x).flat());
665 let segmentedNodeUnglue = segmentedNodesFixed.map((x)=>this.removeGlue(x, frame).flat());
666
667 for (var i=0; i<segmentedNodeUnglue.length; i++){
668 var currentLineSkip = baseLineskip;
669 var glyphMaxHeight = this.getGlyphMaxHeight(segmentedNodesFixed[i]);
670 if (currentLineSkip === null || glyphMaxHeight >currentLineSkip ){
671 currentLineSkip = glyphMaxHeight;
672 }
673
674 var currentLineBox : Box = {
675 x : null,
676 y : null,
677 textStyle : defaultTextStyle,
678 direction : frame.directionInsideLine,
679 width : frame.width,
680 height : currentLineSkip,
681 content : <Box[]>segmentedNodeUnglue[i],
682 }
683
684 bigBoxContent.push(currentLineBox);
685
686 }
687
688 bigBox.content = bigBoxContent;
689
690 return bigBox;
691 }
692
693 /**
694 * get the max height of the glyph`[a, b, c]`
695 * @param nodeLine the node line [a, b, c, ...]
696 * @returns
697 */
698 getGlyphMaxHeight(nodeLine : BoxesItem[]) : number{
699 let segmentedNodeLineHeight = nodeLine.map((x : BoxesItem)=>{if ("height" in x && x.height > 0.0){return x.height}else{return 0.0}});
700 let maxHeight = Math.max(...segmentedNodeLineHeight);
701 return maxHeight;
702 }
703
704 removeGlue(nodeLine : BoxesItem[], frame : FrameBox) : BoxesItem[]{
705 let breakLineAlgorithms = new breakLines.BreakLineAlgorithm();
706 let glueRemoved = nodeLine.filter((x)=>!breakLineAlgorithms.isHGlue(x));
707 let onlyGlue = nodeLine.filter((x)=>breakLineAlgorithms.isHGlue(x));
708 let sumStretchFactor = onlyGlue.map((x)=>{if("stretchFactor" in x){ return x.stretchFactor} else{return 0;}})
709 .reduce((acc, cur)=>acc+cur , 0);
710
711 let glueRemovedWidth = glueRemoved.map((x)=>{if("width" in x){ return x.width} else{return 0;}})
712 .reduce((acc, cur)=>acc+cur , 0);
713 let offset = frame.width * 0.75 - glueRemovedWidth;
714 var res = [];
715 for (var i=0; i<nodeLine.length; i++){
716 var ele = nodeLine[i];
717 if (breakLineAlgorithms.isHGlue(ele)){
718 let tmp : Box = {
719 x : null,
720 y : null,
721 textStyle : null,
722 direction : frame.directionInsideLine,
723 //width : 0, // ragged
724 width : ele.stretchFactor / sumStretchFactor * offset,
725 height : 0,
726 content : "",
727
728 }
729
730 res.push(tmp);
731 }else{
732 res.push(ele);
733 }
734 }
735
736 return res;
737 }
738
739 /**
740 * remove breakpoints
741 * @param boxitemline boxitem in a line with a breakpoint
742 * @returns boxitemline with break points removed
743 */
744 removeBreakPoints(boxitemline : BoxesItem[]) : BoxesItem[]{
745 var res : BoxesItem[] = [];
746 let breakLineAlgorithms = new breakLines.BreakLineAlgorithm();
747
748 for (var i = 0; i<boxitemline.length; i++){
749 let ele = boxitemline[i];
750 if (breakLineAlgorithms.isBreakPoint(ele)){
751 if (i == boxitemline.length-1){
752 res.push(ele.newLined);
753 }else{
754 res.push(ele.original);
755 }
756 }else{
757 res.push(ele);
758 }
759 }
760
761 return res;
762 }
763
764
765 }
766
767
768 /*
769 export let a = new Clo();
770 export default a; */