]> git.kianting.info Git - clo/blob - src/libclo/index.ts
add grid
[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 import "pdfkit";
9 import PDFKitPage from "pdfkit/js/page";
10 import { ColorTypes, PDFDocument, rgb } from "pdf-lib";
11 import * as fs from "fs";
12
13 /**
14 * TYPES
15 */
16
17 /**
18 * text direction
19 * LTR - left to right
20 * TTB - top to bottom
21 * etc.
22 */
23 export enum Direction{
24 LTR,
25 RTL,
26 TTB,
27 BTT,
28 }
29
30 /**
31 * Horizonal glue.
32 * - stretchFactor : the stretch factor in float
33 */
34 export interface HGlue{
35 stretchFactor: number
36 }
37
38 export interface BreakPoint{
39 original : BoxesItem,
40 newLined : BoxesItem
41 }
42
43 export type BoxesItem = HGlue | Box | BreakPoint | BoxesItem[] ;
44
45 /**
46 * frame box is a subclass of box
47 * - directionInsideLine : text direction inside a line
48 * - baselineskip : the distance between baselines in px
49 */
50 export interface FrameBox extends Box{
51 directionInsideLine : Direction,
52 baseLineskip : number | null,
53 }
54
55 export interface CharBox extends Box{
56 minX: number,
57 maxX: number,
58 minY: number,
59 maxY: number,
60
61 }
62
63 /**
64 * a basic Box
65 * - x :
66 * - y :
67 * - textStyle :
68 * - direction :
69 * - width : x_advance
70 * - content :
71 */
72 export interface Box{
73 x : number | null,
74 y : number | null,
75 textStyle : TextStyle | null,
76 direction : Direction,
77 width : number,
78 height : number,
79 content : string | Box[] | null,
80 }
81
82
83 /**
84 * DEFAULT CONST PART
85 */
86 export const A4_IN_PX = {"width" : 793.7,
87 "height" : 1122.5};
88
89 export const defaultTextStyle : TextStyle = {
90 family : "FreeSerif",
91 size : ptToPx(12),
92 textWeight : TextWeight.REGULAR,
93 fontStyle : FontStyle.ITALIC,
94 }
95
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,
105 content : null,
106 };
107
108 /**
109 * definition for cjk scripts
110 * - Hani : Han Character
111 * - Hang : Hangul
112 * - Bopo : Bopomofo
113 * - Kana : Katakana
114 * - Hira : Hiragana
115 */
116 export const cjkvBlocksInRegex = ["Hani", "Hang", "Bopo", "Kana", "Hira"];
117
118 export const cjkvRegexPattern = new RegExp("((?:" +
119 cjkvBlocksInRegex.map((x)=>"\\p{Script_Extensions="+x+"}").join("|") + ")+)", "gu");
120 /**
121 * FUNCTION PART
122 */
123 /**
124 * convert from ptToPx
125 * @param pt pt size value
126 * @returns the corresponding px value
127 */
128 export function ptToPx(pt : number) : number{
129 return pt * 4.0 / 3.0;
130 }
131
132
133
134 /**
135 * REGISTER PART
136 */
137
138 /**
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
143 */
144 export function twoReturnsToNewline(arr : tkTree, clo : Clo): tkTree{
145 var middle : tkTree = [];
146
147 for (let i = 0; i < arr.length; i++) {
148 var item = arr[i];
149 if (!Array.isArray(item)){
150 middle = middle.concat(item.split(/(\n\n)/g));
151 }
152 else{
153 middle.push(item);
154 }
155 }
156
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`
162 }
163 else{
164 result.push(middle[j]);
165 }
166 }
167
168 return result;
169 }
170
171 /**
172 * split CJKV and non-CJKV
173 *
174 * @param arr : input tkTree
175 * @returns a splitted tkTree (by CJK and NonCJK)
176 * - Examples:
177 * ```
178 * [`many臺中daylight`] => [`many`, `臺中`, `dahylight`]
179 * ```
180 */
181 export function splitCJKV(arr : tkTree, clo : Clo): tkTree{
182 var result : tkTree = [];
183 for (let i = 0; i < arr.length; i++) {
184 var item = arr[i];
185 if (!Array.isArray(item)){
186 result = result.concat(item.split(cjkvRegexPattern));
187 }
188 else{
189 result.push(item);
190 }
191 }
192
193 return result;
194 }
195
196 /**
197 * hyphenation for a clo document
198 * @param arr the array for a `tkTree`
199 * @param clo the Clo object
200 */
201 export function hyphenForClo(arr : tkTree, clo : Clo): tkTree{
202 let hyphenLanguage : string = clo.attrs["hyphenLanguage"];
203 let res = hyphenTkTree(arr, hyphenLanguage);
204 return res;
205
206 }
207
208 /**
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
214 */
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++){
219 var item = arr[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] , "" ]);
223 }
224 else{
225 result.push(item);
226 }
227 }
228
229 return result;
230 }
231
232 /**
233 * remove all the `` (empty string) in the arr
234 * @param arr the tkTree to be filtered
235 * @param clo the Clo file
236 */
237 export function filterEmptyString(arr : tkTree, clo : Clo) : tkTree{
238 if (Array.isArray(arr)){
239 arr.filter((x)=>{return x != ``;});
240 }
241
242 return arr;
243 }
244
245
246 /**
247 * OTHER FUNCTIONS
248 */
249
250 /**
251 * hyphenate for a tkTree
252 * - hyphenation => ["bp", "", "-"]
253 * @param arr the tkTree array
254 * @param lang ISO 639 code for the language
255 */
256 export function hyphenTkTree(arr : tkTree, lang: string) : tkTree{
257 // import corresponding hyphen language data and function
258 let hyphen = require("hyphen/"+lang);
259
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
272 }
273 newSplitted.push(hyphenatedSplitted[hyphenatedSplitted.length-1]);
274
275 result = result.concat(newSplitted);
276
277 }else{
278 result.push(element);
279 }
280
281 }
282
283 return result;
284 }
285
286 /**
287 * calculate the text width and Height with a given `TextStyle`
288 * @param preprocessed
289 * @param defaultFontStyle
290 */
291 export async function calculateTextWidthHeight(element : tkTree, style : TextStyle): Promise<BoxesItem[]> {
292 var res = [];
293
294 for (var i=0; i<element.length; i++){
295 res.push(await calculateTextWidthHeightAux(element[i], style));
296 }
297
298 res = res.flat();
299
300 return res;
301 }
302
303
304 /**
305 * calculate the text width and Height with a given `TextStyle`
306 * @param preprocessed
307 * @param defaultFontStyle
308 */
309 export async function calculateTextWidthHeightAux(element : tkTree, style : TextStyle): Promise<BoxesItem> {
310 var result : BoxesItem = [];
311
312
313
314 let fontPair = fontStyleTofont(style);
315 if (fontPair.path.match(/\.ttc$/)){
316 var font = await fontkit.openSync(fontPair.path, fontPair.psName);
317 }
318 else{
319 var font = await fontkit.openSync(fontPair.path);
320 }
321 if (!Array.isArray(element)){
322 var run = font.layout(element, undefined, undefined, undefined, "ltr");
323
324
325
326 for (var j=0;j<run.glyphs.length;j++){
327 let runGlyphsItem = run.glyphs[j];
328
329
330 let item : CharBox = {
331 x : null,
332 y : null,
333 textStyle : style,
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
342 }
343
344 result.push(item);
345
346 }
347 return result;
348
349
350
351
352 }else if(element[0] == "bp"){
353
354 var beforeNewLine = await calculateTextWidthHeightAux(element[1], style);
355 if (Array.isArray(beforeNewLine)){
356 beforeNewLine = beforeNewLine.flat();
357 }
358
359 let afterNewLine = await calculateTextWidthHeightAux(element[2], style);
360 if (Array.isArray(afterNewLine)){
361 afterNewLine = afterNewLine.flat();
362 }
363
364 let breakPointNode : BreakPoint = {
365 original : beforeNewLine,
366 newLined : afterNewLine,
367 }
368
369 return breakPointNode;
370 }else if(element[0] == "hglue" && !Array.isArray(element[1])){
371 let hGlue : HGlue = {stretchFactor : parseFloat(element[1])}
372 return hGlue;
373 }
374 else{
375 return calculateTextWidthHeight(element, style);
376 }
377 }
378
379
380
381
382 /**
383 * whole document-representing class
384 */
385 export class Clo{
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)
392
393
394 constructor(){
395 this.preprocessors = [];
396 this.mainStream = [];
397 this.attrs = {
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)
401 };
402
403
404
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);
411 }
412
413 public setAttr(attr : string, val : any):void{
414 Object.assign(this.attrs, attr, val);
415 }
416
417 public getAttr(attr:string) : any{
418 if (Object.keys(this.attrs).length === 0){
419 return this.attrs[attr];
420 }else{
421 return undefined;
422 }
423
424 }
425
426 /**
427 * register a function of preprocessor
428 * @param f a function
429 */
430 public preprocessorRegister(f : Function){
431 this.preprocessors.push(f);
432 }
433
434 public async generatePdf(){
435 // preprocessed
436 var preprocessed = this.mainStream;
437 for (var i = 0; i<this.preprocessors.length; i++){
438 preprocessed = this.preprocessors[i](preprocessed, this);
439 }
440 // generate the width and height of the stream
441
442 let defaultFontStyle : TextStyle = this.attrs["defaultFrameStyle"].textStyle;
443 let a = await calculateTextWidthHeight(preprocessed, defaultFontStyle);
444
445 let breakLineAlgorithms = new breakLines.BreakLineAlgorithm();
446 // TODO
447 //console.log(breakLineAlgorithms.totalCost(a,70));
448 let segmentedNodes = breakLineAlgorithms.segmentedNodes(a, 70);
449
450 console.log(
451 this.segmentedNodesToFrameBox(segmentedNodes, <FrameBox>this.attrs["defaultFrameStyle"]));
452
453 // generate pdf
454 const pdfDoc = await PDFDocument.create();
455 var page = pdfDoc.addPage();
456 page.drawText('You can create PDFs!');
457
458 for (var j = 0; j<1000; j+=5){
459 if (j %50 == 0){
460 page.drawText(i.toString(), {x: 50, y: j});
461 }
462
463 page.drawLine({
464 start: { x: 0, y: j },
465 end: { x: 1000, y: j },
466 thickness: 0.5,
467 color: rgb(0.75, 0.2, 0.2),
468 opacity: 0.20,
469 });
470 }
471
472 for (var i = 0; i<1000; i+=5){
473 if (i % 50 == 0){
474 page.drawText(i.toString(), {x: i, y: 50});
475 }
476 page.drawLine({
477 start: { x: i, y: 0 },
478 end: { x: i, y: 1000 },
479 thickness: 0.5,
480 color: rgb(0.75, 0.2, 0.2),
481 opacity: 0.20,
482 });
483 }
484 pdfDoc.save();
485
486 const pdfBytes = await pdfDoc.save();
487 fs.writeFileSync("blank.pdf", pdfBytes);
488 }
489
490 segmentedNodesToFrameBox(segmentedNodes : BoxesItem[][], frame : FrameBox) : Box{
491 let baseLineskip = frame.baseLineskip;
492 let boxArrayEmpty : Box[] = [];
493 let bigBox : Box = {
494 x : frame.x,
495 y : frame.y,
496 textStyle : frame.textStyle,
497 direction : frame.direction,
498 width : frame.width,
499 height : frame.height,
500 content : boxArrayEmpty,
501 }
502
503 var bigBoxContent : Box[] = boxArrayEmpty;
504
505 let segmentedNodesFixed = segmentedNodes.map((x)=>this.removeBreakPoints
506 (x).flat());
507 let segmentedNodeUnglue = segmentedNodesFixed.map((x)=>this.removeGlue(x, frame).flat());
508
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;
514 }
515
516 var currentLineBox : Box = {
517 x : null,
518 y : null,
519 textStyle : defaultTextStyle,
520 direction : frame.directionInsideLine,
521 width : frame.width,
522 height : currentLineSkip,
523 content : <Box[]>segmentedNodeUnglue[i],
524 }
525
526 bigBoxContent.push(currentLineBox);
527
528 }
529
530 bigBox.content = bigBoxContent;
531
532 return bigBox;
533 }
534
535 /**
536 * get the max height of the glyph`[a, b, c]`
537 * @param nodeLine the node line [a, b, c, ...]
538 * @returns
539 */
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);
543 return maxHeight;
544 }
545
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);
552
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;
556 var res = [];
557 for (var i=0; i<nodeLine.length; i++){
558 var ele = nodeLine[i];
559 if (breakLineAlgorithms.isHGlue(ele)){
560 let tmp : Box = {
561 x : null,
562 y : null,
563 textStyle : null,
564 direction : frame.directionInsideLine,
565 width : ele.stretchFactor / sumStretchFactor * offset,
566 height : 0,
567 content : "",
568
569 }
570
571 res.push(tmp);
572 }else{
573 res.push(ele);
574 }
575 }
576
577 return res;
578 }
579
580 /**
581 * remove breakpoints
582 * @param boxitemline boxitem in a line with a breakpoint
583 * @returns boxitemline with break points removed
584 */
585 removeBreakPoints(boxitemline : BoxesItem[]) : BoxesItem[]{
586 var res : BoxesItem[] = [];
587 let breakLineAlgorithms = new breakLines.BreakLineAlgorithm();
588
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);
594 }else{
595 res.push(ele.original);
596 }
597 }else{
598 res.push(ele);
599 }
600 }
601
602 return res;
603 }
604
605
606 }
607
608
609 /*
610 export let a = new Clo();
611 export default a; */