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