add text measurement

This commit is contained in:
Tan, Kian-ting 2024-04-19 01:25:07 +08:00
parent 64bd9bae26
commit 90fd2d0d5c
9 changed files with 393 additions and 63 deletions

BIN
3rdparty/harfbuzzjs/hb.wasm vendored Executable file

Binary file not shown.

View file

@ -14,4 +14,5 @@
- [ ] close pdf
- [v] add character
- [ ] add path
- [ ] basic typesetting format
- [ ] basic typesetting format
- [v] text measuring width in pt

11
package-lock.json generated
View file

@ -10,6 +10,7 @@
"license": "MIT",
"dependencies": {
"@pdf-lib/fontkit": "^1.1.1",
"harfbuzzjs": "^0.3.5",
"pdf-lib": "^1.17.1",
"typescript-parsec": "^0.3.4"
},
@ -1021,6 +1022,11 @@
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
"dev": true
},
"node_modules/harfbuzzjs": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/harfbuzzjs/-/harfbuzzjs-0.3.5.tgz",
"integrity": "sha512-SbNxmVAyhlUJTHdaxgK5S6Uqy4mXIu80Vl6KDn8d+ctPAF6W3DY2yehB4BwIC24I/Tk5HGLjaQkyny5gY0r41Q=="
},
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@ -2464,6 +2470,11 @@
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
"dev": true
},
"harfbuzzjs": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/harfbuzzjs/-/harfbuzzjs-0.3.5.tgz",
"integrity": "sha512-SbNxmVAyhlUJTHdaxgK5S6Uqy4mXIu80Vl6KDn8d+ctPAF6W3DY2yehB4BwIC24I/Tk5HGLjaQkyny5gY0r41Q=="
},
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",

View file

@ -17,6 +17,7 @@
"license": "MIT",
"dependencies": {
"@pdf-lib/fontkit": "^1.1.1",
"harfbuzzjs": "^0.3.5",
"pdf-lib": "^1.17.1",
"typescript-parsec": "^0.3.4"
},

View file

@ -157,6 +157,43 @@ SINGLE.setPattern((0, typescript_parsec_2.alt)((0, typescript_parsec_2.apply)((0
LISPS.setPattern((0, typescript_parsec_2.alt)((0, typescript_parsec_2.apply)((0, typescript_parsec_2.kmid)((0, typescript_parsec_2.seq)((0, typescript_parsec_2.str)("("), __), (0, typescript_parsec_2.rep_sc)(LISP), (0, typescript_parsec_2.str)(")")), applyList), (0, typescript_parsec_2.apply)((0, typescript_parsec_2.kright)((0, typescript_parsec_2.str)("'"), (0, typescript_parsec_2.kmid)((0, typescript_parsec_2.seq)((0, typescript_parsec_2.str)("("), __), (0, typescript_parsec_2.rep_sc)(LISP), (0, typescript_parsec_2.str)(")"))), applyQuoted)));
CON_STR_INNER.setPattern((0, typescript_parsec_2.alt)((0, typescript_parsec_2.apply)((0, typescript_parsec_2.tok)(TokenKind.Id), tokenToStr), (0, typescript_parsec_2.apply)((0, typescript_parsec_2.tok)(TokenKind.Int), tokenToStr), (0, typescript_parsec_2.apply)((0, typescript_parsec_2.tok)(TokenKind.Flo), tokenToStr), (0, typescript_parsec_2.apply)((0, typescript_parsec_2.tok)(TokenKind.Str), tokenToStr), (0, typescript_parsec_2.apply)((0, typescript_parsec_2.tok)(TokenKind.Other), tokenToStr), (0, typescript_parsec_2.apply)((0, typescript_parsec_2.tok)(TokenKind.SpaceNL), tokenToStr), (0, typescript_parsec_2.apply)((0, typescript_parsec_2.kright)((0, typescript_parsec_2.tok)(TokenKind.BSlash), (0, typescript_parsec_2.tok)(TokenKind.LParen)), tokenToStr), (0, typescript_parsec_2.apply)((0, typescript_parsec_2.kright)((0, typescript_parsec_2.tok)(TokenKind.BSlash), (0, typescript_parsec_2.tok)(TokenKind.RParen)), tokenToStr), (0, typescript_parsec_2.apply)((0, typescript_parsec_2.kright)((0, typescript_parsec_2.tok)(TokenKind.BSlash), (0, typescript_parsec_2.tok)(TokenKind.LBrack)), tokenToStr), (0, typescript_parsec_2.apply)((0, typescript_parsec_2.kright)((0, typescript_parsec_2.tok)(TokenKind.BSlash), (0, typescript_parsec_2.tok)(TokenKind.RBrack)), tokenToStr), (0, typescript_parsec_2.apply)((0, typescript_parsec_2.kright)((0, typescript_parsec_2.tok)(TokenKind.BSlash), (0, typescript_parsec_2.tok)(TokenKind.Apos)), tokenToStr), (0, typescript_parsec_2.apply)((0, typescript_parsec_2.kright)((0, typescript_parsec_2.tok)(TokenKind.BSlash), (0, typescript_parsec_2.tok)(TokenKind.BSlash)), bSlashTokenToStr), LISPS));
CON_STR.setPattern((0, typescript_parsec_2.apply)((0, typescript_parsec_2.kmid)((0, typescript_parsec_2.str)("["), (0, typescript_parsec_2.rep_sc)(CON_STR_INNER), (0, typescript_parsec_2.str)("]")), applyStrings));
/**
* measuer the width of a test in px
* @param inputString the string to be measured
* @param fontFamily font family name
* @param fontSizePt font size in pt
* @returns the width in px
*/
async function measureWidthPx(inputString, fontFamily, fontSizePt) {
return await WebAssembly.instantiate(fs.readFileSync(__dirname + "/../3rdparty/harfbuzzjs/hb.wasm"))
.then(function (wsm) {
var hb = require('harfbuzzjs/hbjs');
hb = hb(wsm.instance);
let fontName = (0, child_process_1.spawnSync)('fc-match', ['--format=%{file}', fontFamily]);
const fontPath = fontName.stdout.toString();
let fontdata = fs.readFileSync(fontPath);
var blob = hb.createBlob(fontdata); // Load the font data into something Harfbuzz can use
var face = hb.createFace(blob, 0); // Select the first font in the file (there's normally only one!)
var font = hb.createFont(face); // Create a Harfbuzz font object from the face
font.setScale(fontSizePt * 4 / 3 * 1000, fontSizePt * 4 / 3 * 1000);
var buffer = hb.createBuffer(); // Make a buffer to hold some text
buffer.addText(inputString); // Fill it with some stuff
buffer.guessSegmentProperties(); // Set script, language and direction
hb.shape(font, buffer); // Shape the text, determining glyph IDs and positions
var output = buffer.json();
var totalX = 0;
for (var glyph of output) {
var xAdvance = glyph.ax;
totalX += xAdvance;
}
// Release memory
buffer.destroy();
font.destroy();
face.destroy();
blob.destroy();
return totalX / 1000;
});
}
function astToString(ast, isInQuoted) {
if (Array.isArray(ast)) {
const ast2 = ast.map((x) => astToString(x, isInQuoted));
@ -369,11 +406,8 @@ async function drawText(pageIndex, fontFamily, textSize, color, x, y, text) {
const path = fcMatch.stdout.toString();
pdfDoc.registerFontkit(fontkit_1.default);
const fontBytes = fs.readFileSync(path);
console.log("A2A", (0, pdf_lib_1.rgb)(0, 0, 0));
const customFont = await pdfDoc.embedFont(fontBytes);
console.log("A3A", (0, pdf_lib_1.rgb)(0, 0, 0));
const rgbColor = await hexColorToRGB(color);
console.log("A4A", (0, pdf_lib_1.rgb)(0, 0, 0));
let a = await pdfDoc.getPage(0).drawText(text, {
x: x,
y: y,
@ -384,10 +418,10 @@ async function drawText(pageIndex, fontFamily, textSize, color, x, y, text) {
await pdfDoc.save();
}
async function hexColorToRGB(hex) {
let rgbHex = /[#]?(\d{2})(\d{2})(\d{2})/.exec(hex);
let r = parseInt(rgbHex[1], 16) / 256.0;
let g = parseInt(rgbHex[2], 16) / 256.0;
let b = parseInt(rgbHex[3], 16) / 256.0;
let rgbHex = /[#]?([\dA-Fa-f]{2})([\dA-Fa-f]{2})([\dA-Fa-f]{2})/.exec(hex);
let r = parseInt(rgbHex[1], 16) / 255.0;
let g = parseInt(rgbHex[2], 16) / 255.0;
let b = parseInt(rgbHex[3], 16) / 255.0;
return (0, pdf_lib_1.rgb)(r, g, b);
}
function listRef(l, i) {
@ -463,6 +497,24 @@ async function interp(prog, env) {
};
}
}
// define manipulation
if (op.id === "define") {
const vari = prog[1];
const data = await interp(prog[2], env);
if (prog.length !== 3) {
throw invalidLengthException('define', 2);
}
else if (!isItem(vari) || !isItem(data)) {
throw new Error("the type of replace and variable should be the same.");
}
else if (env[vari.id] !== undefined) {
throw new Error("variable can't be duplicated defined.");
}
else {
env = extendEnv(env, vari.id, true, data);
return { type: ItemType.Unit };
}
}
/** let function */
else if (op.id === "let" || op.id === "letrec") {
const bindings = prog[1];
@ -531,9 +583,13 @@ async function interp(prog, env) {
}
}
else {
const argsMapped = await Promise.all(prog.slice(1).map(async (x) => {
return interp(x, env);
}));
let argsMapped = [];
for (var i = 1; i < prog.length; i++) {
argsMapped.push(await interp(prog[i], env));
}
/* const argsMapped = await Promise.all( prog.slice(1).map(async (x) => {
return interp(x, env);
})); */
// binary basic operator
if (op.id === "+") {
return interpBinary(add, argsMapped);
@ -604,6 +660,73 @@ async function interp(prog, env) {
}
}
}
else if (op.id === "and") {
if (prog.length !== 3) {
throw invalidLengthException('and', 2);
}
else if (!argsMapped[0].hasOwnProperty('type') || argsMapped[0].type !== ItemType.Bool
|| !argsMapped[1].hasOwnProperty('type') || argsMapped[1].type !== ItemType.Bool) {
throw new Error("the arg of 'and' is not valid boolean value");
}
else {
let ret = {
type: ItemType.Bool,
bool: argsMapped[0].bool && argsMapped[1].bool
};
return ret;
}
}
else if (op.id === "or") {
if (prog.length !== 3) {
throw invalidLengthException('or', 2);
}
else if (!argsMapped[0].hasOwnProperty('type') || argsMapped[0].type !== ItemType.Bool
|| !argsMapped[1].hasOwnProperty('type') || argsMapped[1].type !== ItemType.Bool) {
throw new Error("the arg of 'or' is not valid boolean value");
}
else {
let ret = {
type: ItemType.Bool,
bool: argsMapped[0].bool || argsMapped[1].bool
};
return ret;
}
}
// measuring
else if (op.id === "measureWidthPx") {
if (prog.length !== 4) {
throw invalidLengthException('measureWidthPx', 3);
}
else {
let text = argsMapped[0].str;
let fontfamily = argsMapped[1].str;
let sizePt = argsMapped[2].flo;
let returnValue = await measureWidthPx(text, fontfamily, sizePt);
return {
type: ItemType.Flo,
flo: returnValue
};
}
}
else if (op.id === "isList") {
const arg = argsMapped[0];
if (prog.length !== 2) {
throw invalidLengthException('isList', 1);
}
else if (arg.type === ItemType.Ls) {
let a = {
type: ItemType.Bool,
bool: true,
};
return a;
}
else {
return {
type: ItemType.Bool,
bool: false,
};
}
}
else if (op.id === "car") {
const arg = argsMapped[0];
if (prog.length !== 2) {
@ -631,7 +754,7 @@ async function interp(prog, env) {
else if (op.id === "cons") {
const arg = argsMapped;
if (prog.length !== 3) {
throw invalidLengthException('cdr', 2);
throw invalidLengthException('cons', 2);
}
else if (!arg[1].hasOwnProperty('type') || arg[1].type !== ItemType.Ls) {
throw new Error("the 2nd arg of 'cons' is not a list.");
@ -693,7 +816,7 @@ async function interp(prog, env) {
// set manipulations
else if (op.id === "set!") {
const vari = prog[1];
const replacer = prog[2];
const replacer = await interp(prog[2], env);
if (prog.length !== 3) {
throw invalidLengthException('set!', 2);
}

View file

@ -20,7 +20,7 @@ import {
tok,
opt,
} from "typescript-parsec";
import {inspect} from "node:util";
/** input lisp file */
@ -265,6 +265,55 @@ CON_STR.setPattern(
apply(kmid(str("["), rep_sc(CON_STR_INNER), str("]")), applyStrings)
);
/**
* measuer the width of a test in px
* @param inputString the string to be measured
* @param fontFamily font family name
* @param fontSizePt font size in pt
* @returns the width in px
*/
async function measureWidthPx(inputString: string, fontFamily : string, fontSizePt: number): Promise<number>{
return await WebAssembly.instantiate(fs.readFileSync(__dirname+"/../3rdparty/harfbuzzjs/hb.wasm"))
.then(function (wsm) {
var hb = require('harfbuzzjs/hbjs');
hb = hb(wsm.instance);
let fontName = spawnSync('fc-match', ['--format=%{file}', fontFamily]);
const fontPath = fontName.stdout.toString();
let fontdata = fs.readFileSync(fontPath);
var blob = hb.createBlob(fontdata); // Load the font data into something Harfbuzz can use
var face = hb.createFace(blob, 0); // Select the first font in the file (there's normally only one!)
var font = hb.createFont(face); // Create a Harfbuzz font object from the face
font.setScale(fontSizePt * 4/3* 1000 , fontSizePt*4/3 * 1000 );
var buffer = hb.createBuffer(); // Make a buffer to hold some text
buffer.addText(inputString); // Fill it with some stuff
buffer.guessSegmentProperties(); // Set script, language and direction
hb.shape(font, buffer); // Shape the text, determining glyph IDs and positions
var output : Array<{g : number,
ax : number,
dx : number,
dy : number}> = buffer.json();
var totalX = 0;
for (var glyph of output) {
var xAdvance = glyph.ax;
totalX += xAdvance;
}
// Release memory
buffer.destroy();
font.destroy();
face.destroy();
blob.destroy();
return totalX / 1000;
});
}
function astToString(ast: AST, isInQuoted? : boolean): string {
if (Array.isArray(ast)) {
const ast2 = ast.map((x)=>astToString(x, isInQuoted));
@ -480,13 +529,10 @@ const fcMatch = await spawnSync('fc-match', ['--format=%{file}', fontFamily]);
const path = fcMatch.stdout.toString();
pdfDoc.registerFontkit(fontkit);
const fontBytes = fs.readFileSync(path);
console.log("A2A",rgb(0,0,0));
const customFont = await pdfDoc.embedFont(fontBytes);
console.log("A3A",rgb(0,0,0));
const rgbColor = await hexColorToRGB(color);
console.log("A4A",rgb(0,0,0));
let a = await pdfDoc.getPage(0).drawText(text, {
x: x,
@ -501,10 +547,10 @@ const path = fcMatch.stdout.toString();
async function hexColorToRGB(hex: string): Promise<RGB>{
let rgbHex = /[#]?(\d{2})(\d{2})(\d{2})/.exec(hex);
let r = parseInt((rgbHex as RegExpExecArray)[1], 16)/256.0;
let g = parseInt((rgbHex as RegExpExecArray)[2], 16)/256.0;
let b = parseInt((rgbHex as RegExpExecArray)[3], 16)/256.0;
let rgbHex = /[#]?([\dA-Fa-f]{2})([\dA-Fa-f]{2})([\dA-Fa-f]{2})/.exec(hex);
let r = parseInt((rgbHex as RegExpExecArray)[1], 16)/255.0;
let g = parseInt((rgbHex as RegExpExecArray)[2], 16)/255.0;
let b = parseInt((rgbHex as RegExpExecArray)[3], 16)/255.0;
return rgb(r,g,b);
}
@ -587,6 +633,22 @@ async function interp(prog: AST, env: Env): Promise<AST> {
}
}
}
// define manipulation
if (op.id === "define") {
const vari : ItemId = prog[1] as ItemId;
const data = await interp(prog[2], env);
if (prog.length !== 3){
throw invalidLengthException('define', 2);
}else if (!isItem(vari) || !isItem(data)){
throw new Error("the type of replace and variable should be the same.")
}else if(env[vari.id] !== undefined){
throw new Error("variable can't be duplicated defined.")
}else {
env = extendEnv(env, vari.id, true, data);
return {type:ItemType.Unit};
}
}
/** let function */
else if (op.id === "let" || op.id === "letrec"){
const bindings = prog[1];
@ -647,10 +709,16 @@ async function interp(prog: AST, env: Env): Promise<AST> {
}
}
else{
let argsMapped = [];
for (var i=1;i<prog.length;i++){
argsMapped.push(await interp(prog[i], env));
}
const argsMapped = await Promise.all( prog.slice(1).map(async (x) => {
/* const argsMapped = await Promise.all( prog.slice(1).map(async (x) => {
return interp(x, env);
}));
})); */
// binary basic operator
if (op.id === "+") {
return interpBinary(add, argsMapped);
@ -706,7 +774,69 @@ async function interp(prog: AST, env: Env): Promise<AST> {
};
}
}
} else if (op.id === "car") {
}else if (op.id === "and"){
if (prog.length !== 3){
throw invalidLengthException('and', 2);
}else if (!argsMapped[0].hasOwnProperty('type') || (argsMapped[0] as Item).type !== ItemType.Bool
|| !argsMapped[1].hasOwnProperty('type') || (argsMapped[1] as Item).type !== ItemType.Bool){
throw new Error("the arg of 'and' is not valid boolean value")
}else{
let ret = {
type : ItemType.Bool,
bool: (argsMapped[0] as ItemBool).bool && (argsMapped[1] as ItemBool).bool
};
return ret as Item;
}
}else if (op.id === "or"){
if (prog.length !== 3){
throw invalidLengthException('or', 2);
}else if (!argsMapped[0].hasOwnProperty('type') || (argsMapped[0] as Item).type !== ItemType.Bool
|| !argsMapped[1].hasOwnProperty('type') || (argsMapped[1] as Item).type !== ItemType.Bool){
throw new Error("the arg of 'or' is not valid boolean value")
}else{
let ret = {
type : ItemType.Bool,
bool: (argsMapped[0] as ItemBool).bool || (argsMapped[1] as ItemBool).bool
};
return ret as Item;
}
}
// measuring
else if (op.id === "measureWidthPx"){
if (prog.length !== 4){
throw invalidLengthException('measureWidthPx', 3);
}else{
let text = (argsMapped[0] as ItemStr).str;
let fontfamily = (argsMapped[1] as ItemStr).str;
let sizePt = (argsMapped[2] as ItemFlo).flo;
let returnValue = await measureWidthPx(text, fontfamily, sizePt);
return {
type: ItemType.Flo,
flo: returnValue
}
}
}
else if (op.id === "isList"){
const arg = argsMapped[0];
if (prog.length !== 2){
throw invalidLengthException('isList', 1);
}else if ((arg as Item).type === ItemType.Ls){
let a = {
type: ItemType.Bool,
bool: true as boolean,
};
return a as Item;
}else{
return {
type: ItemType.Bool,
bool: false as boolean,
};
}
}
else if (op.id === "car") {
const arg = argsMapped[0];
if (prog.length !== 2){
throw invalidLengthException('car', 1);
@ -728,7 +858,7 @@ async function interp(prog: AST, env: Env): Promise<AST> {
}else if (op.id === "cons") {
const arg = argsMapped;
if (prog.length !== 3){
throw invalidLengthException('cdr', 2);
throw invalidLengthException('cons', 2);
}else if (!arg[1].hasOwnProperty('type') || (arg[1] as Item).type !== ItemType.Ls){
throw new Error("the 2nd arg of 'cons' is not a list.")
}else{
@ -778,11 +908,10 @@ async function interp(prog: AST, env: Env): Promise<AST> {
return subString(str, i as ItemInt, prog[3] as ItemInt);}
}
}
// set manipulations
else if (op.id === "set!") {
const vari : ItemId = prog[1] as ItemId;
const replacer = prog[2];
const replacer = await interp(prog[2], env);
if (prog.length !== 3){
throw invalidLengthException('set!', 2);
}else if (!isItem(vari) || !isItem(replacer)
@ -968,6 +1097,7 @@ async function run(){
const prog = fs.readFileSync(filename, { encoding: 'utf8' });
console.log(await evaluate(prog));
const pdfBytes = await pdfDoc.save();
fs.writeFileSync(filename+'.pdf', pdfBytes, 'binary');

View file

@ -1,39 +1,65 @@
(letrec (
(defaultFontFormat
'(("fontFamily" "Gentium")
("color" "#000000")
("size" 12)
)
)
(map (lambda (f l)
(if (!= l '())
(cons (f (car l)) (map f (cdr l)))
'())))
(emptyDict '())
(extendDict (lambda (dict var data) (cons (cons var (cons data '())) dict)))
(dictRef (lambda (dict key)
(if (= dict '()) false
(if (= key (car (car dict))) (car (cdr (car dict))) (dictRef (cdr dict) key))
)))
)
(begin
(addPDFPage '())
(drawText
(dictRef defaultFontFormat "fontFamily")
(dictRef defaultFontFormat "size")
(dictRef defaultFontFormat "color")
40.0
50.0
"blah"
(define defaultFontFormat
'(("fontFamily" "Gentium")
("color" "#ff0000")
("fontSize" 12)
)
)
(addPDFPage '())
(map (lambda (x) (+ x 2)) '(8 9 10))
(let ((dict emptyDict))
(let ((dictExtended
(extendDict
(extendDict emptyDict 1 2) 2 4)))
(dictRef dictExtended 2)
))))
(define map (lambda (f l)
(if (!= l '())
(cons (f (car l)) (map f (cdr l)))
'())))
(define emptyDict '())
(define extendDict (lambda (dict var data) (cons (cons var (cons data '())) dict)))
(define dictRef (lambda (dict key)
(if (= dict '()) false
(if (= key (car (car dict))) (car (cdr (car dict))) (dictRef (cdr dict) key))
)))
(define setDictItem (lambda (dict key data)
(if (= (dictRef dict key) false)
false
(setDictItemAux dict '() key data)
)))
(define setDictItemAux (lambda (oldDict newDict key data)
(if (= oldDict '()) newDict
(if (= (car(car oldDict)) key)
(setDictItemAux (cdr oldDict) (cons (cons key (cons data '())) newDict) key data)
(setDictItemAux (cdr oldDict) (cons (car oldDict) newDict) key data)
))))
(addPDFPage '())
(addPDFPage '())
(define text2boxAux2 (lambda (format text)
(if (isList text)
(if (= (listRef text 0) "fontSize")
(let ((newFormat (setDictItem format "fontSize" (listRef text 1)))) (text2boxAux1 newFormat (listRef text 2)))
text)
(cons format (cons text '())))
))
(define text2boxAux1 (lambda (format txt)
(if (isList txt)
(map (lambda (x) (text2boxAux2 format x)) txt)
(cons format (cons txt '()))
)))
(define text2box (lambda (txt) (text2boxAux1 defaultFontFormat txt)))
(drawText
(dictRef defaultFontFormat "fontFamily")
(dictRef defaultFontFormat "fontSize")
(dictRef defaultFontFormat "color")
40.0
50.0
"blah"
)
(define text '("abracabra" ("fontSize" 18 "貓") "foo"))
(text2box text)
(measureWidthPx "1314abc" "Gentium" 12.0)
)

Binary file not shown.

38
text2.lisp Normal file
View file

@ -0,0 +1,38 @@
(letrec (
(defaultFontFormat
'(("fontFamily" "Gentium")
("color" "#ff0000")
("size" 12)
)
)
(map (lambda (f l)
(if (!= l '())
(cons (f (car l)) (map f (cdr l)))
'())))
(emptyDict '())
(extendDict (lambda (dict var data) (cons (cons var (cons data '())) dict)))
(dictRef (lambda (dict key)
(if (= dict '()) false
(if (= key (car (car dict))) (car (cdr (car dict))) (dictRef (cdr dict) key))
)))
)
(begin
(addPDFPage '())
(drawText
"Gentium"
"#ff0000"
"12"
40.0
50.0
"blah"
)
(addPDFPage '())
(map (lambda (x) (+ x 2)) '(8 9 10))
(let ((dict emptyDict))
(let ((dictExtended
(extendDict
(extendDict emptyDict 1 2) 2 4)))
(dictRef dictExtended 2)
))))