]>
git.kianting.info Git - clo/blob - 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 import {memfs
} from
"memfs";
22 export enum Direction
{
30 * - stretchFactor : the stretch factor in float
32 export interface HGlue
{
33 isHorizonalGlue
: true,
37 export interface VGlue
{
38 isHorizonalGlue
: false,
42 export interface BreakPoint
{
47 /** BR is like html br */
48 export interface BR
extends BreakPoint
{
52 export type BoxesItem
= HGlue
| Box
| BreakPoint
| BoxesItem
[] ;
55 * frame box is a subclass of box
56 * - directionInsideLine : text direction inside a line
57 * - baselineskip : the distance between baselines in px
59 export interface FrameBox
extends Box
{
60 directionInsideLine
: Direction
,
61 baseLineskip
: number | null,
64 export interface CharBox
extends Box
{
78 * - width : x_advance pt
84 textStyle
: TextStyle
| null,
85 direction
: Direction
,
88 content
: string | Box
[] | null,
95 export const A4_IN_PX
= {"width" : 793.7,
98 export const defaultTextStyle
: TextStyle
= {
99 family
: "Noto Sans CJK TC",
101 textWeight
: TextWeight
.REGULAR
,
102 fontStyle
: FontStyle
.ITALIC
,
105 export const defaultFrameStyle
: FrameBox
= {
106 directionInsideLine
: Direction
.LTR
,
107 direction
: Direction
.TTB
,
108 baseLineskip
: ptToPx(15),
109 textStyle
: defaultTextStyle
,
110 x
: A4_IN_PX
.width
* 0.10 ,
111 y
: A4_IN_PX
.height
* 0.10 ,
112 width
: A4_IN_PX
.width
* 0.80 ,
113 height
: A4_IN_PX
.height
* 0.80 ,
118 * definition for cjk scripts
119 * - Hani : Han Character
125 export const cjkvBlocksInRegex
= ["Hani", "Hang", "Bopo", "Kana", "Hira"];
127 export const cjkvRegexPattern
= new RegExp("((?:" +
128 cjkvBlocksInRegex
.map((x
)=>"\\p{Script_Extensions="+x
+"}").join("|") + ")+)", "gu");
133 * convert from ptToPx
134 * @param pt pt size value
135 * @returns the corresponding px value
137 export function ptToPx(pt
: number) : number{
138 return pt
* 4.0 / 3.0;
148 * convert '\n\n' to new paragraph command ["br"]
149 * @param arr the input `tkTree`
150 * @param clo the `Clo` object
151 * @returns the input tktree
153 export function twoReturnsToNewline(arr
: tkTree
, clo
: Clo
): tkTree
{
154 var middle
: tkTree
= [];
156 for (let i
= 0; i
< arr
.length
; i
++) {
158 if (!Array.isArray(item
)){
159 middle
= middle
.concat(item
.split(/(\n\n)/g
));
166 var result
: tkTree
= [];
167 for (let j
= 0; j
< middle
.length
; j
++){
168 var item
= middle
[j
];
169 if (!Array.isArray(item
) && item
== "\n\n"){
170 result
.push(["br"]); // push a newline command to the result `tkTree`
173 result
.push(middle
[j
]);
181 * split CJKV and non-CJKV
183 * @param arr : input tkTree
184 * @returns a splitted tkTree (by CJK and NonCJK)
187 * [`many臺中daylight`] => [`many`, `臺中`, `dahylight`]
190 export function splitCJKV(arr
: tkTree
, clo
: Clo
): tkTree
{
191 var result
: tkTree
= [];
192 for (let i
= 0; i
< arr
.length
; i
++) {
194 if (!Array.isArray(item
)){
195 result
= result
.concat(item
.split(cjkvRegexPattern
));
206 * hyphenation for a clo document
207 * @param arr the array for a `tkTree`
208 * @param clo the Clo object
210 export function hyphenForClo(arr
: tkTree
, clo
: Clo
): tkTree
{
211 let hyphenLanguage
: string = clo
.attrs
["hyphenLanguage"];
212 let res
= hyphenTkTree(arr
, hyphenLanguage
);
218 * convert spaces to Breakpoint
219 * \s+ => ["bp" [\s+] ""]
220 * @param arr the tkTree input text stream
221 * @param clo the Clo object
222 * @returns the converted object
224 export function spacesToBreakpoint(arr
: tkTree
, clo
: Clo
) : tkTree
{
225 let spacePattern
= /^([ \t]+)$
/g
;
226 var result
: tkTree
= [];
227 for (let i
= 0; i
< arr
.length
; i
++){
229 if (!Array.isArray(item
) && item
.match(spacePattern
)){
230 // push a breakpoint command to the result `tkTree`
231 result
.push([ 'bp', [["hglue", "0.1"], item
] , "" ]);
242 * remove all the `` (empty string) in the arr
243 * @param arr the tkTree to be filtered
244 * @param clo the Clo file
246 export function filterEmptyString(arr
: tkTree
, clo
: Clo
) : tkTree
{
247 if (Array.isArray(arr
)){
248 arr
.filter((x
)=>{return x
!= ``;});
260 * hyphenate for a tkTree
261 * - hyphenation => ["bp", "", "-"]
262 * @param arr the tkTree array
263 * @param lang ISO 639 code for the language
265 export function hyphenTkTree(arr
: tkTree
, lang
: string) : tkTree
{
266 // import corresponding hyphen language data and function
267 let hyphen
= require("hyphen/"+lang
);
269 let result
:tkTree
[] = [];
270 for (let i
= 0; i
< arr
.length
; i
++) {
271 let element
= arr
[i
];
272 let splitter
= "分"; // a CJKV
273 if (!Array.isArray(element
)){
274 let hyphenatedElement
: string = hyphen
.hyphenateSync(element
, {hyphenChar
:splitter
});
275 let hyphenatedSplitted
: tkTree
= hyphenatedElement
.split(splitter
);
276 var newSplitted
: tkTree
= [];
277 for (var j
=0; j
<hyphenatedSplitted
.length
-1;j
++){
278 newSplitted
.push(hyphenatedSplitted
[j
]);
279 // "bp" for breakpoint
280 newSplitted
.push(["bp", "", "-"]); //insert a breakable point (bp) mark
282 newSplitted
.push(hyphenatedSplitted
[hyphenatedSplitted
.length
-1]);
284 result
= result
.concat(newSplitted
);
287 result
.push(element
);
298 * calculate the text width and Height with a given `TextStyle`
299 * @param preprocessed
300 * @param defaultFontStyle
302 export async function calculateTextWidthHeight(element
: tkTree
, style
: TextStyle
): Promise
<BoxesItem
[]> {
307 for (var i
=0; i
<element
.length
; i
++){
308 let item
= await calculateTextWidthHeightAux(element
[i
], style
, <TextStyle
>styleCache
, <fontkit
.Font
>fontCache
);
309 styleCache
= item
[1];
321 * calculate the text width and Height with a given `TextStyle`
322 * @param preprocessed
323 * @param defaultFontStyle
325 export async function calculateTextWidthHeightAux(element
: tkTree
,
327 styleCache
: TextStyle
,
328 fontCache
: fontkit
.Font
): Promise
<[BoxesItem
, TextStyle
, fontkit
.Font
] > {
329 var result
: BoxesItem
= [];
332 if (style
=== styleCache
){
337 let fontPair
= fontStyleTofont(style
);
339 if (fontPair
.path
.match(/\
.ttc$
/)){
340 font
= await fontkit
.openSync(fontPair
.path
, fontPair
.psName
);
346 font
= await fontkit
.openSync(fontPair
.path
);
356 if (!Array.isArray(element
)){
357 var run
= font
.layout(element
, undefined, undefined, undefined, "ltr");
361 for (var j
=0;j
<run
.glyphs
.length
;j
++){
362 let runGlyphsItem
= run
.glyphs
[j
];
365 let item
: CharBox
= {
369 direction
: Direction
.LTR
,
370 width
: (runGlyphsItem
.advanceWidth
)*(style
.size
)/1000 * 0.75, // in pt
371 height
: (runGlyphsItem
.bbox
.maxY
- runGlyphsItem
.bbox
.minY
)*(style
.size
)/1000 * 0.75, // in pt
372 content
: element
[j
],
373 minX
: runGlyphsItem
.bbox
.minX
,
374 maxX
: runGlyphsItem
.bbox
.maxX
,
375 minY
: runGlyphsItem
.bbox
.minY
,
376 maxY
: runGlyphsItem
.bbox
.maxY
382 return [result
, styleCache
, fontCache
];
386 // break point of a line
387 }else if(element
[0] == "bp"){
390 var beforeNewLine
= (await calculateTextWidthHeightAux(element
[1], style
, styleCache
, fontCache
))[0];
391 if (Array.isArray(beforeNewLine
)){
392 beforeNewLine
= beforeNewLine
.flat();
395 let afterNewLine
= (await calculateTextWidthHeightAux(element
[2], style
, styleCache
, fontCache
))[0];
396 if (Array.isArray(afterNewLine
)){
397 afterNewLine
= afterNewLine
.flat();
400 let breakPointNode
: BreakPoint
= {
401 original
: beforeNewLine
,
402 newLined
: afterNewLine
,
406 return [breakPointNode
, styleCache
, fontCache
];
408 }else if(element
[0] == "hglue" && !Array.isArray(element
[1])){
409 let hGlue
: HGlue
= {
410 isHorizonalGlue
: true,
411 stretchFactor
: parseFloat(element
[1])}
412 return [hGlue
, styleCache
, fontCache
];
415 else if(element
[0] == "br"){
416 let brBoxItem
= await calculateTextWidthHeightAux(["hglue", "10000"],
417 style
, styleCache
, fontCache
);
421 original
: brBoxItem
[0],
422 newLined
: brBoxItem
[0]};
423 return [BR
, styleCache
, fontCache
];
426 return [await calculateTextWidthHeight(element
, style
), styleCache
, fontCache
];
431 * put childrenBox inside VBox
433 export function putInVBox(childrenBox
: Box
[], parentBox
: Box
) : Box
{
434 var voffset
= Array(childrenBox
.length
).fill(0);
436 for (var i
=0;i
<childrenBox
.length
-1;i
++){
437 voffset
[i
+1] = voffset
[i
] + childrenBox
[i
].height
;
440 console
.log("~", voffset
);
441 for (var i
=0; i
<childrenBox
.length
; i
++){
442 childrenBox
[i
] = applyVOffset(childrenBox
[i
], voffset
[i
]);
443 childrenBox
[i
].y
+= voffset
[i
];
446 parentBox
.content
= childrenBox
;
451 * apply vertical offset to a box
452 * @param box the box to be applied
453 * @param voffset the vertical offset
454 * @returns applied box
456 export function applyVOffset(box
: Box
, voffset
: number){
460 if (Array.isArray(box
.content
)){
461 box
.content
= box
.content
.map((x
)=>applyVOffset(x
, voffset
));
467 * whole document-representing class
470 /** storing the text string into the main frame */
471 mainStream
: Array<string>;
472 /** array of preprocessor functions to preprocess the `mainStream` */
473 preprocessors
: Array<Function>;
474 /** the attributes for the Clo */
475 attrs
: {[index
: string]:any} ; // a4 size(x,y)
479 this.preprocessors
= [];
480 this.mainStream
= [];
482 "page" : A4_IN_PX
, // default for a4. in px of [x, y]
483 "defaultFrameStyle" : defaultFrameStyle
, // defaultFrameStyle
484 "hyphenLanguage" : 'en' // hyphenated in the language (in ISO 639)
489 // register the precessor functions
490 this.preprocessorRegister(splitCJKV
);
491 this.preprocessorRegister(hyphenForClo
);
492 this.preprocessorRegister(twoReturnsToNewline
);
493 this.preprocessorRegister(spacesToBreakpoint
);
494 this.preprocessorRegister(filterEmptyString
);
497 public setAttr(attr
: string, val
: any):void{
498 Object.assign(this.attrs
, attr
, val
);
501 public getAttr(attr
:string) : any{
502 if (Object.keys(this.attrs
).length
=== 0){
503 return this.attrs
[attr
];
511 * register a function of preprocessor
512 * @param f a function
514 public preprocessorRegister(f
: Function){
515 this.preprocessors
.push(f
);
518 public async generatePdf(){
521 var preprocessed
= this.mainStream
;
522 for (var i
= 0; i
<this.preprocessors
.length
; i
++){
523 preprocessed
= this.preprocessors
[i
](preprocessed
, this);
526 // generate the width and height of the stream
528 let defaultFontStyle
: TextStyle
= this.attrs
.defaultFrameStyle
.textStyle
;
530 // calculate the width and height of each chars
531 let calculated
= await calculateTextWidthHeight(preprocessed
, defaultFontStyle
);
534 let paragraphized
= this.paragraphize(calculated
);
537 let breakLineAlgorithms
= new breakLines
.BreakLineAlgorithm();
539 let segmentedNodes
= paragraphized
.map((x
)=>breakLineAlgorithms
.segmentedNodes(x
, this.attrs
.defaultFrameStyle
.width
));
541 let segmentedNodesToBox
= segmentedNodes
.map((x
)=>
542 this.segmentedNodesToFrameBoxAux(x
, <FrameBox
>this.attrs
.defaultFrameStyle
));
544 let boxWithParagraph
= putInVBox(segmentedNodesToBox
, this.attrs
.defaultFrameStyle
);
546 console
.log(boxWithParagraph
);
548 // fix the bug of main Frame x & y
549 if(boxWithParagraph
.x
!== null)
550 {boxWithParagraph
.x
*= 0.75}
551 if(boxWithParagraph
.y
!== null)
552 {boxWithParagraph
.y
*= 0.75}
554 let boxesFixed
= this.fixenBoxesPosition(boxWithParagraph
);
557 (<Box
[]>boxesFixed
.content
).map((e
)=>{console
.log(e
.y
)});
561 const doc
= new PDFDocument({size
: 'A4'});
562 doc
.pipe(fs
.createWriteStream('output.pdf'));
565 let styleCache
: any = {};
566 let fontPairCache
: fontPathPSNamePair
= {path
: "", psName
: ""};
567 await this.putText(doc
, boxesFixed
, <TextStyle
>styleCache
, fontPairCache
);
574 paragraphize(calculated
: BoxesItem
[]): BoxesItem
[][]{
575 var res
: BoxesItem
[][] = [[]];
576 for (var i
=0;i
<calculated
.length
;i
++){
577 if ("isBR" in <Box
>(calculated
[i
])){
578 res
[res
.length
-1] = res
[res
.length
-1].concat(calculated
[i
]);
581 res
[res
.length
-1] = res
[res
.length
-1].concat(calculated
[i
]);
585 res
= res
.filter((x
)=>x
.length
!== 0);
589 async putText(doc
: PDFKit
.PDFDocument
, box
: Box
, styleCache
: TextStyle
,
590 fontPairCache
: fontPathPSNamePair
):
591 Promise
<[PDFKit
.PDFDocument
, TextStyle
, fontPathPSNamePair
]>{
595 if (box
.textStyle
!== null){
597 if(box
.textStyle
== styleCache
){
598 fontPair
= fontPairCache
;
600 fontPair
= fontStyleTofont(box
.textStyle
);
601 styleCache
= box
.textStyle
;
602 fontPairCache
= fontPair
;
603 let textColor
= box
.textStyle
.color
;
605 if (fontPair
.path
.match(/\
.ttc$
/g
)){
607 .fillColor(textColor
!== undefined ? textColor
: "#000000")
608 .font(fontPair
.path
, fontPair
.psName
)
609 .fontSize(box
.textStyle
.size
* 0.75);}
612 .fillColor(textColor
!== undefined ? textColor
: "#000000")
614 .fontSize(box
.textStyle
.size
* 0.75); // 0.75 must added!
618 if (box
.textStyle
.color
!== undefined){
619 doc
.fill(box
.textStyle
.color
);
622 if (Array.isArray(box
.content
)){
623 for (var k
=0; k
<box
.content
.length
; k
++){
625 let tmp
= await this.putText(doc
, box
.content
[k
], styleCache
, fontPairCache
);
628 fontPairCache
= tmp
[2];
630 }else if (box
.content
!== null){
631 await doc
.text(box
.content
,
632 (box
.x
!==null? box
.x
: undefined),
633 (box
.y
!==null? box
.y
: undefined));
639 return [doc
, styleCache
, fontPairCache
];
644 private grid(doc
: any) {
645 for (var j
= 0; j
< A4_IN_PX
.width
; j
+= 5) {
647 doc
.save().fill('#000000')
648 .fontSize(8).text(j
.toString(), j
*0.75, 50);
653 .strokeColor("#dddddd")
655 .lineTo(j
*0.75, 1000)
662 .strokeColor("#dddddd")
664 .lineTo(j
*0.75, 1000)
668 for (var i
= 0; i
< 1050; i
+= 5) {
671 .fontSize(8).text(i
.toString(), 50, i
*0.75);
676 .strokeColor("#bbbbbb")
678 .lineTo(1000, i
*0.75)
684 .strokeColor("#bbbbbb")
686 .lineTo(1000, i
*0.75)
697 * make all the nest boxes's position fixed
698 * @param box the main boxes
699 * @returns the fixed boxes
701 fixenBoxesPosition(box
: Box
) : Box
{
702 var currX
: number = (box
.x
!==null?box
.x
:0); // current x
703 var currY
: number =(box
.y
!==null?box
.y
:0); // current y
704 if (Array.isArray(box
.content
)){
705 for (var i
=0; i
<box
.content
.length
; i
++){
706 if (box
.direction
== Direction
.LTR
){
707 box
.content
[i
].x
= currX
;
708 box
.content
[i
].y
= currY
;
709 let elementWidth
= box
.content
[i
].width
;
710 if(elementWidth
!== null){
711 currX
+= elementWidth
;
715 if (box
.direction
== Direction
.TTB
){
716 box
.content
[i
].x
= currX
;
717 box
.content
[i
].y
= currY
;
718 let elementHeight
= box
.content
[i
].height
;
719 if(elementHeight
!== null){
720 currY
+= elementHeight
;
724 box
.content
[i
] = this.fixenBoxesPosition(box
.content
[i
]);
732 * input a `segmentedNodes` and a layed `frame`, return a big `Box` that nodes is put in.
733 * @param segmentedNodes the segmentnodes to be input
734 * @param frame the frame to be layed out.
735 * @returns the big `Box`.
737 segmentedNodesToFrameBoxAux(segmentedNodes
: BoxesItem
[][], frame
: FrameBox
) : Box
{
738 let baseLineskip
= frame
.baseLineskip
;
739 let boxArrayEmpty
: Box
[] = [];
741 x
: (frame
.x
!==null? frame
.x
* 0.75 : null),
742 y
: (frame
.y
!==null? frame
.y
* 0.75 : null),
743 textStyle
: frame
.textStyle
,
744 direction
: frame
.direction
,
746 height
:frame
.height
,
747 content
: boxArrayEmpty
,
750 var bigBoxContent
: Box
[] = boxArrayEmpty
;
752 let segmentedNodesFixed
= segmentedNodes
.map((x
)=>this.removeBreakPoints
754 let segmentedNodeUnglue
= segmentedNodesFixed
.map((x
)=>this.removeGlue(x
, frame
).flat());
756 for (var i
=0; i
<segmentedNodeUnglue
.length
; i
++){
757 var currentLineSkip
= baseLineskip
;
758 var glyphMaxHeight
= this.getGlyphMaxHeight(segmentedNodesFixed
[i
]);
759 if (currentLineSkip
=== null || glyphMaxHeight
>currentLineSkip
){
760 currentLineSkip
= glyphMaxHeight
;
763 var currentLineBox
: Box
= {
766 textStyle
: defaultTextStyle
,
767 direction
: frame
.directionInsideLine
,
769 height
: currentLineSkip
,
770 content
: <Box
[]>segmentedNodeUnglue
[i
],
773 bigBoxContent
.push(currentLineBox
);
777 bigBox
.content
= bigBoxContent
;
778 let bigBoxHeight
= bigBoxContent
.map((x
)=>x
.height
).reduce((x
,y
)=>x
+y
, 0);
779 bigBox
.height
= bigBoxHeight
;
785 * get the max height of the glyph`[a, b, c]`
786 * @param nodeLine the node line [a, b, c, ...]
789 getGlyphMaxHeight(nodeLine
: BoxesItem
[]) : number{
790 let segmentedNodeLineHeight
= nodeLine
.map((x
: BoxesItem
)=>{if ("height" in x
&& x
.height
> 0.0){return x
.height
}else{return 0.0}});
791 let maxHeight
= Math.max(...segmentedNodeLineHeight
);
795 removeGlue(nodeLine
: BoxesItem
[], frame
: FrameBox
) : BoxesItem
[]{
796 let breakLineAlgorithms
= new breakLines
.BreakLineAlgorithm();
797 let glueRemoved
= nodeLine
.filter((x
)=>!breakLineAlgorithms
.isHGlue(x
));
798 let onlyGlue
= nodeLine
.filter((x
)=>breakLineAlgorithms
.isHGlue(x
));
799 let sumStretchFactor
= onlyGlue
.map((x
)=>{if("stretchFactor" in x
){ return x
.stretchFactor
} else{return 0;}})
800 .reduce((acc
, cur
)=>acc
+cur
, 0);
802 let glueRemovedWidth
= glueRemoved
.map((x
)=>{if("width" in x
){ return x
.width
} else{return 0;}})
803 .reduce((acc
, cur
)=>acc
+cur
, 0);
804 let offset
= frame
.width
* 0.75 - glueRemovedWidth
;
806 for (var i
=0; i
<nodeLine
.length
; i
++){
807 var ele
= nodeLine
[i
];
808 if (breakLineAlgorithms
.isHGlue(ele
)){
813 direction
: frame
.directionInsideLine
,
814 //width : 0, // ragged
815 width
: ele
.stretchFactor
/ sumStretchFactor
* offset
,
832 * @param boxitemline boxitem in a line with a breakpoint
833 * @returns boxitemline with break points removed
835 removeBreakPoints(boxitemline
: BoxesItem
[]) : BoxesItem
[]{
836 var res
: BoxesItem
[] = [];
837 let breakLineAlgorithms
= new breakLines
.BreakLineAlgorithm();
839 for (var i
= 0; i
<boxitemline
.length
; i
++){
840 let ele
= boxitemline
[i
];
841 if (breakLineAlgorithms
.isBreakPoint(ele
)){
842 if (i
== boxitemline
.length
-1){
843 res
.push(ele
.newLined
);
845 res
.push(ele
.original
);
860 export let a = new Clo();