]> git.kianting.info Git - clo/blob - src/libclo/index.ts
b64a819c9c719a63d2f7af34d0ec4c73d749fc7a
[clo] / 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";
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,
98 y : A4_IN_PX.height * 0.10,
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)/1000,
332 height : (runGlyphsItem.bbox.maxY - runGlyphsItem.bbox.minY)*(style.size)/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, 70);
445
446 console.log(this.segmentedNodesToFrameBox(segmentedNodes, <FrameBox>this.attrs["defaultFrameStyle"]));
447 }
448
449 segmentedNodesToFrameBox(segmentedNodes : BoxesItem[][], frame : FrameBox) : Box{
450 let baseLineskip = frame.baseLineskip;
451 let boxArrayEmpty : Box[] = [];
452 let bigBox : Box = {
453 x : frame.x,
454 y : frame.y,
455 textStyle : frame.textStyle,
456 direction : frame.direction,
457 width : frame.width,
458 height : frame.height,
459 content : boxArrayEmpty,
460 }
461
462 var bigBoxContent : Box[] = boxArrayEmpty;
463
464 let segmentedNodesFixed = segmentedNodes.map((x)=>this.removeBreakPoints
465 (x).flat());
466 let segmentedNodeUnglue = segmentedNodesFixed.map((x)=>this.removeGlue(x, frame).flat());
467
468 for (var i=0; i<segmentedNodesFixed.length-1; i++){
469 var currentLineSkip = baseLineskip;
470 var glyphMaxHeight = this.getGlyphMaxHeight(segmentedNodesFixed[i]);
471 if (currentLineSkip === null || glyphMaxHeight >currentLineSkip ){
472 currentLineSkip = glyphMaxHeight;
473 }
474
475 var currentLineBox : Box = {
476 x : null,
477 y : null,
478 textStyle : defaultTextStyle,
479 direction : frame.directionInsideLine,
480 width : frame.width,
481 height : currentLineSkip,
482 content : <Box[]>segmentedNodeUnglue[i],
483 }
484
485 bigBoxContent.push(currentLineBox);
486
487 }
488
489 bigBox.content = bigBoxContent;
490
491 return bigBox;
492 }
493
494 /**
495 * get the max height of the glyph`[a, b, c]`
496 * @param nodeLine the node line [a, b, c, ...]
497 * @returns
498 */
499 getGlyphMaxHeight(nodeLine : BoxesItem[]) : number{
500 let segmentedNodeLineHeight = nodeLine.map((x : BoxesItem)=>{if ("height" in x && x.height > 0.0){return x.height}else{return 0.0}});
501 let maxHeight = Math.max(...segmentedNodeLineHeight);
502 return maxHeight;
503 }
504
505 removeGlue(nodeLine : BoxesItem[], frame : FrameBox) : BoxesItem[]{
506 let breakLineAlgorithms = new breakLines.BreakLineAlgorithm();
507 let glueRemoved = nodeLine.filter((x)=>!breakLineAlgorithms.isHGlue(x));
508 let onlyGlue = nodeLine.filter((x)=>breakLineAlgorithms.isHGlue(x));
509 let sumStretchFactor = onlyGlue.map((x)=>{if("stretchFactor" in x){ return x.stretchFactor} else{return 0;}})
510 .reduce((acc, cur)=>acc+cur , 0);
511
512 let glueRemovedWidth = glueRemoved.map((x)=>{if("width" in x){ return x.width} else{return 0;}})
513 .reduce((acc, cur)=>acc+cur , 0);
514 let offset = frame.width - glueRemovedWidth;
515 var res = [];
516 for (var i=0; i<nodeLine.length; i++){
517 var ele = nodeLine[i];
518 if (breakLineAlgorithms.isHGlue(ele)){
519 let tmp : Box = {
520 x : null,
521 y : null,
522 textStyle : null,
523 direction : frame.directionInsideLine,
524 width : ele.stretchFactor / sumStretchFactor * offset,
525 height : 0,
526 content : "",
527
528 }
529
530 res.push(tmp);
531 }else{
532 res.push(ele);
533 }
534 }
535
536 return res;
537 }
538
539 /**
540 * remove breakpoints
541 * @param boxitemline boxitem in a line with a breakpoint
542 * @returns boxitemline with break points removed
543 */
544 removeBreakPoints(boxitemline : BoxesItem[]) : BoxesItem[]{
545 var res : BoxesItem[] = [];
546 let breakLineAlgorithms = new breakLines.BreakLineAlgorithm();
547
548 for (var i = 0; i<boxitemline.length; i++){
549 let ele = boxitemline[i];
550 if (breakLineAlgorithms.isBreakPoint(ele)){
551 if (i == boxitemline.length-1){
552 res.push(ele.newLined);
553 }else{
554 res.push(ele.original);
555 }
556 }else{
557 res.push(ele);
558 }
559 }
560
561 return res;
562 }
563
564
565 }
566
567
568 /*
569 export let a = new Clo();
570 export default a; */