]>
git.kianting.info Git - clo/blob - index.ts
f48f448d9bcabebaeaba7d0b21a60923bf4baf8b
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";
21 export enum Direction
{
30 * - stretchFactor : the stretch factor in float
32 export interface HGlue
{
36 export interface BreakPoint
{
41 export type BoxesItem
= HGlue
| Box
| BreakPoint
| BoxesItem
[] ;
44 * frame box is a subclass of box
45 * - directionInsideLine : text direction inside a line
46 * - baselineskip : the distance between baselines in px
48 export interface FrameBox
extends Box
{
49 directionInsideLine
: Direction
,
50 baseLineskip
: number | null,
53 export interface CharBox
extends Box
{
67 * - width : x_advance pt
73 textStyle
: TextStyle
| null,
74 direction
: Direction
,
77 content
: string | Box
[] | null,
84 export const A4_IN_PX
= {"width" : 793.7,
87 export const defaultTextStyle
: TextStyle
= {
90 textWeight
: TextWeight
.REGULAR
,
91 fontStyle
: FontStyle
.ITALIC
,
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 ,
107 * definition for cjk scripts
108 * - Hani : Han Character
114 export const cjkvBlocksInRegex
= ["Hani", "Hang", "Bopo", "Kana", "Hira"];
116 export const cjkvRegexPattern
= new RegExp("((?:" +
117 cjkvBlocksInRegex
.map((x
)=>"\\p{Script_Extensions="+x
+"}").join("|") + ")+)", "gu");
122 * convert from ptToPx
123 * @param pt pt size value
124 * @returns the corresponding px value
126 export function ptToPx(pt
: number) : number{
127 return pt
* 4.0 / 3.0;
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
142 export function twoReturnsToNewline(arr
: tkTree
, clo
: Clo
): tkTree
{
143 var middle
: tkTree
= [];
145 for (let i
= 0; i
< arr
.length
; i
++) {
147 if (!Array.isArray(item
)){
148 middle
= middle
.concat(item
.split(/(\n\n)/g
));
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`
162 result
.push(middle
[j
]);
170 * split CJKV and non-CJKV
172 * @param arr : input tkTree
173 * @returns a splitted tkTree (by CJK and NonCJK)
176 * [`many臺中daylight`] => [`many`, `臺中`, `dahylight`]
179 export function splitCJKV(arr
: tkTree
, clo
: Clo
): tkTree
{
180 var result
: tkTree
= [];
181 for (let i
= 0; i
< arr
.length
; i
++) {
183 if (!Array.isArray(item
)){
184 result
= result
.concat(item
.split(cjkvRegexPattern
));
195 * hyphenation for a clo document
196 * @param arr the array for a `tkTree`
197 * @param clo the Clo object
199 export function hyphenForClo(arr
: tkTree
, clo
: Clo
): tkTree
{
200 let hyphenLanguage
: string = clo
.attrs
["hyphenLanguage"];
201 let res
= hyphenTkTree(arr
, hyphenLanguage
);
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
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
++){
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
] , "" ]);
231 * remove all the `` (empty string) in the arr
232 * @param arr the tkTree to be filtered
233 * @param clo the Clo file
235 export function filterEmptyString(arr
: tkTree
, clo
: Clo
) : tkTree
{
236 if (Array.isArray(arr
)){
237 arr
.filter((x
)=>{return x
!= ``;});
249 * hyphenate for a tkTree
250 * - hyphenation => ["bp", "", "-"]
251 * @param arr the tkTree array
252 * @param lang ISO 639 code for the language
254 export function hyphenTkTree(arr
: tkTree
, lang
: string) : tkTree
{
255 // import corresponding hyphen language data and function
256 let hyphen
= require("hyphen/"+lang
);
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
271 newSplitted
.push(hyphenatedSplitted
[hyphenatedSplitted
.length
-1]);
273 result
= result
.concat(newSplitted
);
276 result
.push(element
);
287 * calculate the text width and Height with a given `TextStyle`
288 * @param preprocessed
289 * @param defaultFontStyle
291 export async function calculateTextWidthHeight(element
: tkTree
, style
: TextStyle
): Promise
<BoxesItem
[]> {
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];
310 * calculate the text width and Height with a given `TextStyle`
311 * @param preprocessed
312 * @param defaultFontStyle
314 export async function calculateTextWidthHeightAux(element
: tkTree
,
316 styleCache
: TextStyle
,
317 fontCache
: fontkit
.Font
): Promise
<[BoxesItem
, TextStyle
, fontkit
.Font
] > {
318 var result
: BoxesItem
= [];
321 if (style
=== styleCache
){
326 let fontPair
= fontStyleTofont(style
);
328 if (fontPair
.path
.match(/\
.ttc$
/)){
329 font
= await fontkit
.openSync(fontPair
.path
, fontPair
.psName
);
335 font
= await fontkit
.openSync(fontPair
.path
);
345 if (!Array.isArray(element
)){
346 var run
= font
.layout(element
, undefined, undefined, undefined, "ltr");
350 for (var j
=0;j
<run
.glyphs
.length
;j
++){
351 let runGlyphsItem
= run
.glyphs
[j
];
354 let item
: CharBox
= {
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
371 return [result
, styleCache
, fontCache
];
376 }else if(element
[0] == "bp"){
379 var beforeNewLine
= (await calculateTextWidthHeightAux(element
[1], style
, styleCache
, fontCache
))[0];
380 if (Array.isArray(beforeNewLine
)){
381 beforeNewLine
= beforeNewLine
.flat();
384 let afterNewLine
= (await calculateTextWidthHeightAux(element
[2], style
, styleCache
, fontCache
))[0];
385 if (Array.isArray(afterNewLine
)){
386 afterNewLine
= afterNewLine
.flat();
389 let breakPointNode
: BreakPoint
= {
390 original
: beforeNewLine
,
391 newLined
: afterNewLine
,
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
];
400 return [await calculateTextWidthHeight(element
, style
), styleCache
, fontCache
];
408 * whole document-representing class
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)
420 this.preprocessors
= [];
421 this.mainStream
= [];
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)
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
);
438 public setAttr(attr
: string, val
: any):void{
439 Object.assign(this.attrs
, attr
, val
);
442 public getAttr(attr
:string) : any{
443 if (Object.keys(this.attrs
).length
=== 0){
444 return this.attrs
[attr
];
452 * register a function of preprocessor
453 * @param f a function
455 public preprocessorRegister(f
: Function){
456 this.preprocessors
.push(f
);
459 public async generatePdf(){
462 var preprocessed
= this.mainStream
;
463 for (var i
= 0; i
<this.preprocessors
.length
; i
++){
464 preprocessed
= this.preprocessors
[i
](preprocessed
, this);
467 // generate the width and height of the stream
469 let defaultFontStyle
: TextStyle
= this.attrs
.defaultFrameStyle
.textStyle
;
472 let a
= await calculateTextWidthHeight(preprocessed
, defaultFontStyle
);
474 let breakLineAlgorithms
= new breakLines
.BreakLineAlgorithm();
476 let segmentedNodes
= breakLineAlgorithms
.segmentedNodes(a
, this.attrs
.defaultFrameStyle
.width
);
478 let segmentedNodesToBox
=
479 this.segmentedNodesToFrameBox(segmentedNodes
, <FrameBox
>this.attrs
.defaultFrameStyle
);
482 let boxesFixed
= this.fixenBoxesPosition(segmentedNodesToBox
);
488 const doc
= new PDFDocument({size
: 'A4'});
489 doc
.pipe(fs
.createWriteStream('output.pdf'));
492 let styleCache
: any = {};
493 let fontPairCache
: fontPathPSNamePair
= {path
: "", psName
: ""};
494 await this.putText(doc
, boxesFixed
, <TextStyle
>styleCache
, fontPairCache
);
501 async putText(doc
: PDFKit
.PDFDocument
, box
: Box
, styleCache
: TextStyle
,
502 fontPairCache
: fontPathPSNamePair
):
503 Promise
<[PDFKit
.PDFDocument
, TextStyle
, fontPathPSNamePair
]>{
507 if (box
.textStyle
!== null){
509 if(box
.textStyle
== styleCache
){
510 fontPair
= fontPairCache
;
512 fontPair
= fontStyleTofont(box
.textStyle
);
513 styleCache
= box
.textStyle
;
514 fontPairCache
= fontPair
;
516 if (fontPair
.path
.match(/\
.ttc$
/g
)){
518 .font(fontPair
.path
, fontPair
.psName
)
519 .fontSize(box
.textStyle
.size
* 0.75);}
523 .fontSize(box
.textStyle
.size
* 0.75); // 0.75 must added!
527 if (box
.textStyle
.color
!== undefined){
528 doc
.fill(box
.textStyle
.color
);
531 if (Array.isArray(box
.content
)){
532 for (var k
=0; k
<box
.content
.length
; k
++){
534 let tmp
= await this.putText(doc
, box
.content
[k
], styleCache
, fontPairCache
);
537 fontPairCache
= tmp
[2];
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));
548 return [doc
, styleCache
, fontPairCache
];
553 private grid(doc
: any) {
554 for (var j
= 0; j
< A4_IN_PX
.width
; j
+= 5) {
556 doc
.save().fill('#000000')
557 .fontSize(8).text(j
.toString(), j
*0.75, 50);
562 .strokeColor("#dddddd")
564 .lineTo(j
*0.75, 1000)
571 .strokeColor("#dddddd")
573 .lineTo(j
*0.75, 1000)
577 for (var i
= 0; i
< 1050; i
+= 5) {
580 .fontSize(8).text(i
.toString(), 50, i
*0.75);
585 .strokeColor("#bbbbbb")
587 .lineTo(1000, i
*0.75)
593 .strokeColor("#bbbbbb")
595 .lineTo(1000, i
*0.75)
606 * make all the nest boxes's position fixed
607 * @param box the main boxes
608 * @returns the fixed boxes
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
;
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
;
635 box
.content
[i
] = this.fixenBoxesPosition(box
.content
[i
]);
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`.
648 segmentedNodesToFrameBox(segmentedNodes
: BoxesItem
[][], frame
: FrameBox
) : Box
{
649 let baseLineskip
= frame
.baseLineskip
;
650 let boxArrayEmpty
: 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
,
657 height
:frame
.height
,
658 content
: boxArrayEmpty
,
661 var bigBoxContent
: Box
[] = boxArrayEmpty
;
663 let segmentedNodesFixed
= segmentedNodes
.map((x
)=>this.removeBreakPoints
665 let segmentedNodeUnglue
= segmentedNodesFixed
.map((x
)=>this.removeGlue(x
, frame
).flat());
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
;
674 var currentLineBox
: Box
= {
677 textStyle
: defaultTextStyle
,
678 direction
: frame
.directionInsideLine
,
680 height
: currentLineSkip
,
681 content
: <Box
[]>segmentedNodeUnglue
[i
],
684 bigBoxContent
.push(currentLineBox
);
688 bigBox
.content
= bigBoxContent
;
694 * get the max height of the glyph`[a, b, c]`
695 * @param nodeLine the node line [a, b, c, ...]
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
);
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);
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
;
715 for (var i
=0; i
<nodeLine
.length
; i
++){
716 var ele
= nodeLine
[i
];
717 if (breakLineAlgorithms
.isHGlue(ele
)){
722 direction
: frame
.directionInsideLine
,
723 //width : 0, // ragged
724 width
: ele
.stretchFactor
/ sumStretchFactor
* offset
,
741 * @param boxitemline boxitem in a line with a breakpoint
742 * @returns boxitemline with break points removed
744 removeBreakPoints(boxitemline
: BoxesItem
[]) : BoxesItem
[]{
745 var res
: BoxesItem
[] = [];
746 let breakLineAlgorithms
= new breakLines
.BreakLineAlgorithm();
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
);
754 res
.push(ele
.original
);
769 export let a = new Clo();