X-Git-Url: https://git.kianting.info/?a=blobdiff_plain;f=src%2Flibclo%2Findex.ts;h=bcd6a46704d96b05580f4f81eea3e86eef1b214e;hb=134485a6d637ce5b422098c08bccb274c73bca86;hp=7fd58c67868378a167ac574afca354868eaddaab;hpb=796e0c20c767e214b104fa3d17e754ce58da6e73;p=clo diff --git a/src/libclo/index.ts b/src/libclo/index.ts index 7fd58c6..bcd6a46 100644 --- a/src/libclo/index.ts +++ b/src/libclo/index.ts @@ -1,7 +1,14 @@ -import { isKeyObject, isStringObject } from "util/types"; +import { isBoxedPrimitive, isKeyObject, isStringObject } from "util/types"; import {tkTree} from "../parser"; -import {TextStyle, FontStyle, TextWeight} from "../canva"; -import { isString } from "util"; +import {FontStyle, TextStyle, TextWeight, fontStyleTofont} from "../canva"; +import { JSDOM } from "jsdom"; +import * as fontkit from "fontkit"; +import * as util from "node:util"; +import * as breakLines from "./breakLines"; +import "pdfkit"; +import PDFKitPage from "pdfkit/js/page"; +import { ColorTypes, PDFDocument, rgb } from "pdf-lib"; +import * as fs from "fs"; /** * TYPES @@ -13,30 +20,59 @@ import { isString } from "util"; * TTB - top to bottom * etc. */ -enum Direction{ +export enum Direction{ LTR, RTL, TTB, BTT, } +/** + * Horizonal glue. + * - stretchFactor : the stretch factor in float + */ +export interface HGlue{ + stretchFactor: number +} + +export interface BreakPoint{ + original : BoxesItem, + newLined : BoxesItem +} + +export type BoxesItem = HGlue | Box | BreakPoint | BoxesItem[] ; + /** * frame box is a subclass of box * - directionInsideLine : text direction inside a line * - baselineskip : the distance between baselines in px */ -interface FrameBox extends Box{ +export interface FrameBox extends Box{ directionInsideLine : Direction, baseLineskip : number | null, } +export interface CharBox extends Box{ + minX: number, + maxX: number, + minY: number, + maxY: number, + +} + /** * a basic Box + * - x : + * - y : + * - textStyle : + * - direction : + * - width : x_advance + * - content : */ -interface Box{ +export interface Box{ x : number | null, y : number | null, - fontStyle : FontStyle | null, + textStyle : TextStyle | null, direction : Direction, width : number, height : number, @@ -47,21 +83,21 @@ interface Box{ /** * DEFAULT CONST PART */ -const A4_IN_PX = {"width" : 793.7, +export const A4_IN_PX = {"width" : 793.7, "height" : 1122.5}; -const defaultTextStyle : FontStyle = { - family : "FreeSans", - size : 12, +export const defaultTextStyle : TextStyle = { + family : "FreeSerif", + size : ptToPx(12), textWeight : TextWeight.REGULAR, - textStyle : TextStyle.ITALIC, + fontStyle : FontStyle.ITALIC, } -const defaultFrameStyle : FrameBox = { +export const defaultFrameStyle : FrameBox = { directionInsideLine : Direction.LTR, direction : Direction.TTB, baseLineskip : ptToPx(15), - fontStyle : defaultTextStyle, + textStyle : defaultTextStyle, x : A4_IN_PX.width * 0.10, y : A4_IN_PX.height * 0.10, width : A4_IN_PX.width * 0.80, @@ -69,9 +105,17 @@ const defaultFrameStyle : FrameBox = { content : null, }; -const cjkvBlocksInRegex = ["Hani"]; +/** + * definition for cjk scripts + * - Hani : Han Character + * - Hang : Hangul + * - Bopo : Bopomofo + * - Kana : Katakana + * - Hira : Hiragana +*/ +export const cjkvBlocksInRegex = ["Hani", "Hang", "Bopo", "Kana", "Hira"]; -const cjkvRegexPattern = new RegExp("((?:" + +export const cjkvRegexPattern = new RegExp("((?:" + cjkvBlocksInRegex.map((x)=>"\\p{Script_Extensions="+x+"}").join("|") + ")+)", "gu"); /** * FUNCTION PART @@ -81,8 +125,8 @@ const cjkvRegexPattern = new RegExp("((?:" + * @param pt pt size value * @returns the corresponding px value */ -function ptToPx(pt : number) : number{ - return pt * 4 / 3.0; +export function ptToPx(pt : number) : number{ + return pt * 4.0 / 3.0; } @@ -91,19 +135,54 @@ function ptToPx(pt : number) : number{ * REGISTER PART */ +/** + * convert '\n\n' to newline command ["nl"] + * @param arr the input `tkTree` + * @param clo the `Clo` object + * @returns the input tktree + */ +export function twoReturnsToNewline(arr : tkTree, clo : Clo): tkTree{ + var middle : tkTree = []; + + for (let i = 0; i < arr.length; i++) { + var item = arr[i]; + if (!Array.isArray(item)){ + middle = middle.concat(item.split(/(\n\n)/g)); + } + else{ + middle.push(item); + } + } + + var result : tkTree = []; + for (let j = 0; j < middle.length; j++){ + var item = middle[j]; + if (!Array.isArray(item) && item == "\n\n"){ + result.push(["nl"]); // push a newline command to the result `tkTree` + } + else{ + result.push(middle[j]); + } + } + + return result; +} /** * split CJKV and non-CJKV * * @param arr : input tkTree - * @returns + * @returns a splitted tkTree (by CJK and NonCJK) + * - Examples: + * ``` + * [`many臺中daylight`] => [`many`, `臺中`, `dahylight`] + * ``` */ -function splitCJKV(arr : tkTree): tkTree{ +export function splitCJKV(arr : tkTree, clo : Clo): tkTree{ var result : tkTree = []; for (let i = 0; i < arr.length; i++) { var item = arr[i]; if (!Array.isArray(item)){ - console.log(item.split(cjkvRegexPattern)); result = result.concat(item.split(cjkvRegexPattern)); } else{ @@ -114,30 +193,230 @@ function splitCJKV(arr : tkTree): tkTree{ return result; } +/** + * hyphenation for a clo document + * @param arr the array for a `tkTree` + * @param clo the Clo object + */ +export function hyphenForClo(arr : tkTree, clo : Clo): tkTree{ + let hyphenLanguage : string = clo.attrs["hyphenLanguage"]; + let res = hyphenTkTree(arr, hyphenLanguage); + return res; + +} + +/** + * convert spaces to Breakpoint + * \s+ => ["bp" [\s+] ""] + * @param arr the tkTree input text stream + * @param clo the Clo object + * @returns the converted object + */ +export function spacesToBreakpoint(arr : tkTree, clo : Clo) : tkTree{ + let spacePattern = /^([ \t]+)$/g; + var result : tkTree = []; + for (let i = 0; i < arr.length; i++){ + var item = arr[i]; + if (!Array.isArray(item) && item.match(spacePattern)){ + // push a breakpoint command to the result `tkTree` + result.push([ 'bp', [["hglue", "0.1"], item] , "" ]); + } + else{ + result.push(item); + } + } + + return result; +} + +/** + * remove all the `` (empty string) in the arr + * @param arr the tkTree to be filtered + * @param clo the Clo file + */ +export function filterEmptyString(arr : tkTree, clo : Clo) : tkTree{ + if (Array.isArray(arr)){ + arr.filter((x)=>{return x != ``;}); + } + + return arr; +} + + +/** + * OTHER FUNCTIONS + */ + +/** + * hyphenate for a tkTree + * - hyphenation => ["bp", "", "-"] + * @param arr the tkTree array + * @param lang ISO 639 code for the language + */ +export function hyphenTkTree(arr : tkTree, lang: string) : tkTree{ + // import corresponding hyphen language data and function + let hyphen = require("hyphen/"+lang); + + let result :tkTree[] = []; + for (let i = 0; i < arr.length; i++) { + let element = arr[i]; + let splitter = "分"; // a CJKV + if (!Array.isArray(element)){ + let hyphenatedElement : string = hyphen.hyphenateSync(element, {hyphenChar :splitter}); + let hyphenatedSplitted : tkTree = hyphenatedElement.split(splitter); + var newSplitted : tkTree = []; + for (var j=0; j { + var res = []; + + for (var i=0; i { + var result : BoxesItem = []; + + + + let fontPair = fontStyleTofont(style); + if (fontPair.path.match(/\.ttc$/)){ + var font = await fontkit.openSync(fontPair.path, fontPair.psName); + } + else{ + var font = await fontkit.openSync(fontPair.path); + } + if (!Array.isArray(element)){ + var run = font.layout(element, undefined, undefined, undefined, "ltr"); + + + + for (var j=0;j; + /** array of preprocessor functions to preprocess the `mainStream` */ preprocessors : Array; - attributes: {[index: string]:any} ; // a4 size(x,y) + /** the attributes for the Clo */ + attrs: {[index: string]:any} ; // a4 size(x,y) constructor(){ this.preprocessors = []; this.mainStream = []; - this.attributes = {"page" : A4_IN_PX}; + this.attrs = { + "page" : A4_IN_PX, // default for a4. in px of [x, y] + "defaultFrameStyle" : defaultFrameStyle, // defaultFrameStyle + "hyphenLanguage" : 'en' // hyphenated in the language (in ISO 639) + }; // register the precessor functions this.preprocessorRegister(splitCJKV); + this.preprocessorRegister(hyphenForClo); + this.preprocessorRegister(twoReturnsToNewline); + this.preprocessorRegister(spacesToBreakpoint); + this.preprocessorRegister(filterEmptyString); } public setAttr(attr : string, val : any):void{ - Object.assign(this.attributes, attr, val); + Object.assign(this.attrs, attr, val); } public getAttr(attr:string) : any{ - if (Object.keys(this.attributes).length === 0){ - return this.attributes[attr]; + if (Object.keys(this.attrs).length === 0){ + return this.attrs[attr]; }else{ return undefined; } @@ -152,18 +431,181 @@ export class Clo{ this.preprocessors.push(f); } - public generatePdf(){ + public async generatePdf(){ // preprocessed - var prepro = this.mainStream; + var preprocessed = this.mainStream; for (var i = 0; ithis.attrs["defaultFrameStyle"])); + + // generate pdf + const pdfDoc = await PDFDocument.create(); + var page = pdfDoc.addPage(); + page.drawText('You can create PDFs!'); + + for (var j = 0; j<1000; j+=5){ + if (j %50 == 0){ + page.drawText(i.toString(), {x: 50, y: j}); + } + + page.drawLine({ + start: { x: 0, y: j }, + end: { x: 1000, y: j }, + thickness: 0.5, + color: rgb(0.75, 0.2, 0.2), + opacity: 0.20, + }); + } + + for (var i = 0; i<1000; i+=5){ + if (i % 50 == 0){ + page.drawText(i.toString(), {x: i, y: 50}); + } + page.drawLine({ + start: { x: i, y: 0 }, + end: { x: i, y: 1000 }, + thickness: 0.5, + color: rgb(0.75, 0.2, 0.2), + opacity: 0.20, + }); + } + pdfDoc.save(); + + const pdfBytes = await pdfDoc.save(); + fs.writeFileSync("blank.pdf", pdfBytes); + } + + segmentedNodesToFrameBox(segmentedNodes : BoxesItem[][], frame : FrameBox) : Box{ + let baseLineskip = frame.baseLineskip; + let boxArrayEmpty : Box[] = []; + let bigBox : Box = { + x : frame.x, + y : frame.y, + textStyle : frame.textStyle, + direction : frame.direction, + width : frame.width, + height : frame.height, + content : boxArrayEmpty, + } + + var bigBoxContent : Box[] = boxArrayEmpty; + + let segmentedNodesFixed = segmentedNodes.map((x)=>this.removeBreakPoints +(x).flat()); + let segmentedNodeUnglue = segmentedNodesFixed.map((x)=>this.removeGlue(x, frame).flat()); + + for (var i=0; icurrentLineSkip ){ + currentLineSkip = glyphMaxHeight; + } + + var currentLineBox : Box = { + x : null, + y : null, + textStyle : defaultTextStyle, + direction : frame.directionInsideLine, + width : frame.width, + height : currentLineSkip, + content : segmentedNodeUnglue[i], + } + + bigBoxContent.push(currentLineBox); + + } + + bigBox.content = bigBoxContent; + + return bigBox; + } + + /** + * get the max height of the glyph`[a, b, c]` + * @param nodeLine the node line [a, b, c, ...] + * @returns + */ + getGlyphMaxHeight(nodeLine : BoxesItem[]) : number{ + let segmentedNodeLineHeight = nodeLine.map((x : BoxesItem)=>{if ("height" in x && x.height > 0.0){return x.height}else{return 0.0}}); + let maxHeight = Math.max(...segmentedNodeLineHeight); + return maxHeight; + } + + removeGlue(nodeLine : BoxesItem[], frame : FrameBox) : BoxesItem[]{ + let breakLineAlgorithms = new breakLines.BreakLineAlgorithm(); + let glueRemoved = nodeLine.filter((x)=>!breakLineAlgorithms.isHGlue(x)); + let onlyGlue = nodeLine.filter((x)=>breakLineAlgorithms.isHGlue(x)); + let sumStretchFactor = onlyGlue.map((x)=>{if("stretchFactor" in x){ return x.stretchFactor} else{return 0;}}) + .reduce((acc, cur)=>acc+cur , 0); + + let glueRemovedWidth = glueRemoved.map((x)=>{if("width" in x){ return x.width} else{return 0;}}) + .reduce((acc, cur)=>acc+cur , 0); + let offset = frame.width - glueRemovedWidth; + var res = []; + for (var i=0; i