]>
git.kianting.info Git - clo/blob - src/libclo/index.ts
1 import { isBoxedPrimitive
, isKeyObject
, isStringObject
} from
"util/types";
2 import {tkTree
} from
"../parser";
3 import {FontStyle
, TextStyle
, TextWeight
, fontStyleTofont
} from
"../canva";
4 import { JSDOM
} from
"jsdom";
5 import * as fontkit from
"fontkit";
6 import * as util from
"node:util";
7 import * as breakLines from
"./breakLines";
9 import PDFKitPage from
"pdfkit/js/page";
10 import { ColorTypes
, PDFDocument
, rgb
} from
"pdf-lib";
11 import * as fs from
"fs";
23 export enum Direction
{
32 * - stretchFactor : the stretch factor in float
34 export interface HGlue
{
38 export interface BreakPoint
{
43 export type BoxesItem
= HGlue
| Box
| BreakPoint
| BoxesItem
[] ;
46 * frame box is a subclass of box
47 * - directionInsideLine : text direction inside a line
48 * - baselineskip : the distance between baselines in px
50 export interface FrameBox
extends Box
{
51 directionInsideLine
: Direction
,
52 baseLineskip
: number | null,
55 export interface CharBox
extends Box
{
75 textStyle
: TextStyle
| null,
76 direction
: Direction
,
79 content
: string | Box
[] | null,
86 export const A4_IN_PX
= {"width" : 793.7,
89 export const defaultTextStyle
: TextStyle
= {
92 textWeight
: TextWeight
.REGULAR
,
93 fontStyle
: FontStyle
.ITALIC
,
96 export const defaultFrameStyle
: FrameBox
= {
97 directionInsideLine
: Direction
.LTR
,
98 direction
: Direction
.TTB
,
99 baseLineskip
: ptToPx(15),
100 textStyle
: defaultTextStyle
,
101 x
: A4_IN_PX
.width
* 0.10,
102 y
: A4_IN_PX
.height
* 0.10,
103 width
: A4_IN_PX
.width
* 0.80,
104 height
: A4_IN_PX
.height
* 0.80,
109 * definition for cjk scripts
110 * - Hani : Han Character
116 export const cjkvBlocksInRegex
= ["Hani", "Hang", "Bopo", "Kana", "Hira"];
118 export const cjkvRegexPattern
= new RegExp("((?:" +
119 cjkvBlocksInRegex
.map((x
)=>"\\p{Script_Extensions="+x
+"}").join("|") + ")+)", "gu");
124 * convert from ptToPx
125 * @param pt pt size value
126 * @returns the corresponding px value
128 export function ptToPx(pt
: number) : number{
129 return pt
* 4.0 / 3.0;
139 * convert '\n\n' to newline command ["nl"]
140 * @param arr the input `tkTree`
141 * @param clo the `Clo` object
142 * @returns the input tktree
144 export function twoReturnsToNewline(arr
: tkTree
, clo
: Clo
): tkTree
{
145 var middle
: tkTree
= [];
147 for (let i
= 0; i
< arr
.length
; i
++) {
149 if (!Array.isArray(item
)){
150 middle
= middle
.concat(item
.split(/(\n\n)/g
));
157 var result
: tkTree
= [];
158 for (let j
= 0; j
< middle
.length
; j
++){
159 var item
= middle
[j
];
160 if (!Array.isArray(item
) && item
== "\n\n"){
161 result
.push(["nl"]); // push a newline command to the result `tkTree`
164 result
.push(middle
[j
]);
172 * split CJKV and non-CJKV
174 * @param arr : input tkTree
175 * @returns a splitted tkTree (by CJK and NonCJK)
178 * [`many臺中daylight`] => [`many`, `臺中`, `dahylight`]
181 export function splitCJKV(arr
: tkTree
, clo
: Clo
): tkTree
{
182 var result
: tkTree
= [];
183 for (let i
= 0; i
< arr
.length
; i
++) {
185 if (!Array.isArray(item
)){
186 result
= result
.concat(item
.split(cjkvRegexPattern
));
197 * hyphenation for a clo document
198 * @param arr the array for a `tkTree`
199 * @param clo the Clo object
201 export function hyphenForClo(arr
: tkTree
, clo
: Clo
): tkTree
{
202 let hyphenLanguage
: string = clo
.attrs
["hyphenLanguage"];
203 let res
= hyphenTkTree(arr
, hyphenLanguage
);
209 * convert spaces to Breakpoint
210 * \s+ => ["bp" [\s+] ""]
211 * @param arr the tkTree input text stream
212 * @param clo the Clo object
213 * @returns the converted object
215 export function spacesToBreakpoint(arr
: tkTree
, clo
: Clo
) : tkTree
{
216 let spacePattern
= /^([ \t]+)$
/g
;
217 var result
: tkTree
= [];
218 for (let i
= 0; i
< arr
.length
; i
++){
220 if (!Array.isArray(item
) && item
.match(spacePattern
)){
221 // push a breakpoint command to the result `tkTree`
222 result
.push([ 'bp', [["hglue", "0.1"], item
] , "" ]);
233 * remove all the `` (empty string) in the arr
234 * @param arr the tkTree to be filtered
235 * @param clo the Clo file
237 export function filterEmptyString(arr
: tkTree
, clo
: Clo
) : tkTree
{
238 if (Array.isArray(arr
)){
239 arr
.filter((x
)=>{return x
!= ``;});
251 * hyphenate for a tkTree
252 * - hyphenation => ["bp", "", "-"]
253 * @param arr the tkTree array
254 * @param lang ISO 639 code for the language
256 export function hyphenTkTree(arr
: tkTree
, lang
: string) : tkTree
{
257 // import corresponding hyphen language data and function
258 let hyphen
= require("hyphen/"+lang
);
260 let result
:tkTree
[] = [];
261 for (let i
= 0; i
< arr
.length
; i
++) {
262 let element
= arr
[i
];
263 let splitter
= "分"; // a CJKV
264 if (!Array.isArray(element
)){
265 let hyphenatedElement
: string = hyphen
.hyphenateSync(element
, {hyphenChar
:splitter
});
266 let hyphenatedSplitted
: tkTree
= hyphenatedElement
.split(splitter
);
267 var newSplitted
: tkTree
= [];
268 for (var j
=0; j
<hyphenatedSplitted
.length
-1;j
++){
269 newSplitted
.push(hyphenatedSplitted
[j
]);
270 // "bp" for breakpoint
271 newSplitted
.push(["bp", "", "-"]); //insert a breakable point (bp) mark
273 newSplitted
.push(hyphenatedSplitted
[hyphenatedSplitted
.length
-1]);
275 result
= result
.concat(newSplitted
);
278 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
[]> {
294 for (var i
=0; i
<element
.length
; i
++){
295 res
.push(await calculateTextWidthHeightAux(element
[i
], style
));
305 * calculate the text width and Height with a given `TextStyle`
306 * @param preprocessed
307 * @param defaultFontStyle
309 export async function calculateTextWidthHeightAux(element
: tkTree
, style
: TextStyle
): Promise
<BoxesItem
> {
310 var result
: BoxesItem
= [];
314 let fontPair
= fontStyleTofont(style
);
315 if (fontPair
.path
.match(/\
.ttc$
/)){
316 var font
= await fontkit
.openSync(fontPair
.path
, fontPair
.psName
);
319 var font
= await fontkit
.openSync(fontPair
.path
);
321 if (!Array.isArray(element
)){
322 var run
= font
.layout(element
, undefined, undefined, undefined, "ltr");
326 for (var j
=0;j
<run
.glyphs
.length
;j
++){
327 let runGlyphsItem
= run
.glyphs
[j
];
330 let item
: CharBox
= {
334 direction
: Direction
.LTR
,
335 width
: (runGlyphsItem
.advanceWidth
)*(style
.size
)/1000,
336 height
: (runGlyphsItem
.bbox
.maxY
- runGlyphsItem
.bbox
.minY
)*(style
.size
)/1000,
337 content
: element
[j
],
338 minX
: runGlyphsItem
.bbox
.minX
,
339 maxX
: runGlyphsItem
.bbox
.maxX
,
340 minY
: runGlyphsItem
.bbox
.minY
,
341 maxY
: runGlyphsItem
.bbox
.maxY
352 }else if(element
[0] == "bp"){
354 var beforeNewLine
= await calculateTextWidthHeightAux(element
[1], style
);
355 if (Array.isArray(beforeNewLine
)){
356 beforeNewLine
= beforeNewLine
.flat();
359 let afterNewLine
= await calculateTextWidthHeightAux(element
[2], style
);
360 if (Array.isArray(afterNewLine
)){
361 afterNewLine
= afterNewLine
.flat();
364 let breakPointNode
: BreakPoint
= {
365 original
: beforeNewLine
,
366 newLined
: afterNewLine
,
369 return breakPointNode
;
370 }else if(element
[0] == "hglue" && !Array.isArray(element
[1])){
371 let hGlue
: HGlue
= {stretchFactor
: parseFloat(element
[1])}
375 return calculateTextWidthHeight(element
, style
);
383 * whole document-representing class
386 /** storing the text string into the main frame */
387 mainStream
: Array<string>;
388 /** array of preprocessor functions to preprocess the `mainStream` */
389 preprocessors
: Array<Function>;
390 /** the attributes for the Clo */
391 attrs
: {[index
: string]:any} ; // a4 size(x,y)
395 this.preprocessors
= [];
396 this.mainStream
= [];
398 "page" : A4_IN_PX
, // default for a4. in px of [x, y]
399 "defaultFrameStyle" : defaultFrameStyle
, // defaultFrameStyle
400 "hyphenLanguage" : 'en' // hyphenated in the language (in ISO 639)
405 // register the precessor functions
406 this.preprocessorRegister(splitCJKV
);
407 this.preprocessorRegister(hyphenForClo
);
408 this.preprocessorRegister(twoReturnsToNewline
);
409 this.preprocessorRegister(spacesToBreakpoint
);
410 this.preprocessorRegister(filterEmptyString
);
413 public setAttr(attr
: string, val
: any):void{
414 Object.assign(this.attrs
, attr
, val
);
417 public getAttr(attr
:string) : any{
418 if (Object.keys(this.attrs
).length
=== 0){
419 return this.attrs
[attr
];
427 * register a function of preprocessor
428 * @param f a function
430 public preprocessorRegister(f
: Function){
431 this.preprocessors
.push(f
);
434 public async generatePdf(){
436 var preprocessed
= this.mainStream
;
437 for (var i
= 0; i
<this.preprocessors
.length
; i
++){
438 preprocessed
= this.preprocessors
[i
](preprocessed
, this);
440 // generate the width and height of the stream
442 let defaultFontStyle
: TextStyle
= this.attrs
["defaultFrameStyle"].textStyle
;
443 let a
= await calculateTextWidthHeight(preprocessed
, defaultFontStyle
);
445 let breakLineAlgorithms
= new breakLines
.BreakLineAlgorithm();
447 //console.log(breakLineAlgorithms.totalCost(a,70));
448 let segmentedNodes
= breakLineAlgorithms
.segmentedNodes(a
, 70);
451 this.segmentedNodesToFrameBox(segmentedNodes
, <FrameBox
>this.attrs
["defaultFrameStyle"]));
454 const pdfDoc
= await PDFDocument
.create();
455 var page
= pdfDoc
.addPage();
456 page
.drawText('You can create PDFs!');
458 for (var j
= 0; j
<1000; j
+=5){
460 page
.drawText(j
.toString(), {x
: 50, y
: j
});
464 start
: { x
: 0, y
: j
},
465 end
: { x
: 1000, y
: j
},
467 color
: rgb(0.75, 0.2, 0.2),
472 for (var i
= 0; i
<1000; i
+=5){
474 page
.drawText(i
.toString(), {x
: i
, y
: 50});
477 start
: { x
: i
, y
: 0 },
478 end
: { x
: i
, y
: 1000 },
480 color
: rgb(0.75, 0.2, 0.2),
486 const pdfBytes
= await pdfDoc
.save();
487 fs
.writeFileSync("blank.pdf", pdfBytes
);
490 segmentedNodesToFrameBox(segmentedNodes
: BoxesItem
[][], frame
: FrameBox
) : Box
{
491 let baseLineskip
= frame
.baseLineskip
;
492 let boxArrayEmpty
: Box
[] = [];
496 textStyle
: frame
.textStyle
,
497 direction
: frame
.direction
,
499 height
: frame
.height
,
500 content
: boxArrayEmpty
,
503 var bigBoxContent
: Box
[] = boxArrayEmpty
;
505 let segmentedNodesFixed
= segmentedNodes
.map((x
)=>this.removeBreakPoints
507 let segmentedNodeUnglue
= segmentedNodesFixed
.map((x
)=>this.removeGlue(x
, frame
).flat());
509 for (var i
=0; i
<segmentedNodesFixed
.length
-1; i
++){
510 var currentLineSkip
= baseLineskip
;
511 var glyphMaxHeight
= this.getGlyphMaxHeight(segmentedNodesFixed
[i
]);
512 if (currentLineSkip
=== null || glyphMaxHeight
>currentLineSkip
){
513 currentLineSkip
= glyphMaxHeight
;
516 var currentLineBox
: Box
= {
519 textStyle
: defaultTextStyle
,
520 direction
: frame
.directionInsideLine
,
522 height
: currentLineSkip
,
523 content
: <Box
[]>segmentedNodeUnglue
[i
],
526 bigBoxContent
.push(currentLineBox
);
530 bigBox
.content
= bigBoxContent
;
536 * get the max height of the glyph`[a, b, c]`
537 * @param nodeLine the node line [a, b, c, ...]
540 getGlyphMaxHeight(nodeLine
: BoxesItem
[]) : number{
541 let segmentedNodeLineHeight
= nodeLine
.map((x
: BoxesItem
)=>{if ("height" in x
&& x
.height
> 0.0){return x
.height
}else{return 0.0}});
542 let maxHeight
= Math.max(...segmentedNodeLineHeight
);
546 removeGlue(nodeLine
: BoxesItem
[], frame
: FrameBox
) : BoxesItem
[]{
547 let breakLineAlgorithms
= new breakLines
.BreakLineAlgorithm();
548 let glueRemoved
= nodeLine
.filter((x
)=>!breakLineAlgorithms
.isHGlue(x
));
549 let onlyGlue
= nodeLine
.filter((x
)=>breakLineAlgorithms
.isHGlue(x
));
550 let sumStretchFactor
= onlyGlue
.map((x
)=>{if("stretchFactor" in x
){ return x
.stretchFactor
} else{return 0;}})
551 .reduce((acc
, cur
)=>acc
+cur
, 0);
553 let glueRemovedWidth
= glueRemoved
.map((x
)=>{if("width" in x
){ return x
.width
} else{return 0;}})
554 .reduce((acc
, cur
)=>acc
+cur
, 0);
555 let offset
= frame
.width
- glueRemovedWidth
;
557 for (var i
=0; i
<nodeLine
.length
; i
++){
558 var ele
= nodeLine
[i
];
559 if (breakLineAlgorithms
.isHGlue(ele
)){
564 direction
: frame
.directionInsideLine
,
565 width
: ele
.stretchFactor
/ sumStretchFactor
* offset
,
582 * @param boxitemline boxitem in a line with a breakpoint
583 * @returns boxitemline with break points removed
585 removeBreakPoints(boxitemline
: BoxesItem
[]) : BoxesItem
[]{
586 var res
: BoxesItem
[] = [];
587 let breakLineAlgorithms
= new breakLines
.BreakLineAlgorithm();
589 for (var i
= 0; i
<boxitemline
.length
; i
++){
590 let ele
= boxitemline
[i
];
591 if (breakLineAlgorithms
.isBreakPoint(ele
)){
592 if (i
== boxitemline
.length
-1){
593 res
.push(ele
.newLined
);
595 res
.push(ele
.original
);
610 export let a = new Clo();