//// charms.js // This file contains snippets of code to make PDF documents created by Prince more beautiful // For a description of the features, see https://princexml.com/howcome/2021/guides/ // This is experimental code for demonstration purposes. // You may freely copy and reuse this code // howcome@yeslogic.com var DEBUG=false; var UNIT=0; // 0=mm 1=pt // present number in preferred unit function uni(val) { var ret; switch (UNIT) { case 0: ret = val/2.83464; return ret.toFixed(2)+"mm"; case 1: return val; } } if (typeof Prince != "undefined") { addBoxInfoMethods(); } function consoleLog() { if (DEBUG) { console.log.apply(null,arguments); } } function debug() { DEBUG=true; consoleLog("Debugging on"); } //// turn on multipass Prince.trackBoxes = true; var funcsarr = []; var argsarr = []; function printObject(o) { var out = ''; for (var p in o) { out += p + ': ' + o[p] + '\n'; } console.log(out); } function postlayout() { consoleLog("postlayout starting, there are",funcsarr.length,"functions in the queue"); if (funcsarr.length > 0) { var func = funcsarr.shift(); var arg = argsarr.shift(); consoleLog(" postlayout:",func.name); func(arg); consoleLog(" returning from ",func.name,"re-registering postlayout"); Prince.registerPostLayoutFunc(postlayout); } } function schedule(callback, args) { funcsarr.push(callback); argsarr.push(args); if (args) { consoleLog(" scheduling function",callback.name,"with arguments:",args); } else { consoleLog(" scheduling function",callback.name); } Prince.registerPostLayoutFunc(postlayout); } //// baseline alignment // snap() provides simple baseline aligment in Prince by sizing elements // so that they are a multiple of the line-height. Also, the funcion can push elements downwards so that // the distance to the top of the page box is a multiple of the line height. // // // the arguments to the snap function is a the snap size, and a list of selectors/method pairs // the snap size should be equal to the line-height of the body text // selected elements will be adjusted to that their size is divisible by the snap size // selected elements will be increased in size, either through increasing 'padding', 'margin' or 'height' // the selected elements should all have "break-inside: avoid" set function snap(lh) { var snapSize = topt(lh); var selectors = []; var methods = []; // consoleLog("snap",size); for (var i = 1; i < arguments.length; i++) { if (i % 2) { // odd number, i.e. a selector selectors.push(arguments[i]); // consoleLog("Registering selector",arguments[i]); } else { // even number if ((arguments[i]=="margin") || (arguments[i]=="margin-top") || (arguments[i]=="margin-bottom") || (arguments[i]=="padding") || (arguments[i]=="padding-top") || (arguments[i]=="padding-bottom") || (arguments[i]=="padding-even") || (arguments[i]=="height") || (arguments[i]=="growHeight") ||(arguments[i]=="shrinkHeight") || (arguments[i]=="push") || (arguments[i]=="pull") || (arguments[i]=="info")) { methods.push(arguments[i]); // consoleLog(" method:",arguments[i]); } else { // consoleLog(" method",arguments[i],"not recognized, defaulting to padding"); methods.push("padding"); } } } schedule(snap_postlayout,[snapSize,selectors,methods]); } function snap_postlayout(snap_args) { var snapSize = snap_args[0]; var selectors = snap_args[1]; var methods = snap_args[2]; console.log("snap_postlayout",snapSize,selectors,methods); var dbox = document.body.getPrinceBoxes(); var y1 = dbox[0].y; // using first page var y2 = dbox[0].y - dbox[0].h; consoleLog(" first page, page area height",uni(dbox[0].h)); for(var i=0; i snapSize) { paddingBottom += (gap - snapSize); } else { paddingBottom += gap; } elements[i].style.paddingBottom = paddingBottom+"pt"; // console.log(" setting paddingBottom",paddingBottom+"pt"); } else if (method=="padding-even") { paddingTop += gap/2; paddingBottom += gap/2; elements[i].style.paddingTop = paddingTop+"pt"; elements[i].style.paddingBottom = paddingBottom+"pt"; c } else if (method=="push") { // todo: check if padding/border is non-zero paddingTop += snapSize - (ha % snapSize); elements[i].style.paddingTop = paddingTop+"pt"; } else if (method=="pull") { consoleLog(" ha",ha,"paddingTop",paddingTop, "ha+paddingTop",(ha+paddingTop),"ha%snapSize",(ha%snapSize)); consoleLog(" about to add",(ha%snapSize),"to bottom padding"); paddingBottom += (ha % snapSize); elements[i].style.paddingBottom = paddingBottom+"pt"; } else if (method=="balance") { var newheight = height + gap - (paddingTop + paddingBottom + borderTop + borderBottom + marginTop + marginBottom); elements[i].style.height = newheight+"pt"; consoleLog(" balancing height -- gap",uni(gap),"new styleheight:",uni(newheight),"-- styleheight + padding + border =", uni(newheight+paddingBottom+paddingTop+borderBottom+borderTop)); } else if (method=="info") { consoleLog("INFO ha",ha,"paddingTop",paddingTop, "ha+paddingTop",(ha+paddingTop),"ha%snapSize",(ha%snapSize)); } break; // default: // consoleLog("FRAGMENTATION ALERT! Element spans several pages "); } } } // achieve baseline alignment by adjusting height of element to a multiple of line-heigt + addition // // // function height(sel, snap, add, method) { // height('p', '5mm', '3mm', '+-') function height(sel, snap, add, method) { schedule(height_postlayout, [sel, snap, add, method]); } function height_postlayout(height_args) { var sel = height_args[0]; var snap = topt(height_args[1]); var add = topt(height_args[2]); var method = height_args[3]; var elements = document.querySelectorAll(sel); for(var i=0; i (snap/2)) { newheight = height + (snap - diff); } else { newheight = height - diff; } if (DEBUG) { consoleLog("--- height",uni(height),"paB",uni(paddingBottom),"paT",uni(paddingTop),"boB",uni(borderBottom),"boT",uni(borderTop),"maB",uni(marginBottom),"maT",uni(marginTop)); consoleLog("---> newheight ",uni(newheight), uni(diff), uni(snap)); } elements[i].style.height = newheight+"pt"; } } } // '15pt' -> 15, '20px' -> 15. function topt(str) { var resarr; var result = 0; if (resarr = /(\d+)pt/.exec(str)) { result = resarr[1]; } else if (resarr = /(\d+)px/.exec(str)) { result = resarr[1] * 0.75; } else if (resarr = /(\d+)mm/.exec(str)) { result = resarr[1] * 2.83464; } else if (resarr = /(\d+)cm/.exec(str)) { result = resarr[1] * 28.3464; } else if (resarr = /(\d+)in/.exec(str)) { result = resarr[1] * 72; } else if (resarr = /(\d+)/.exec(str)) { result = resarr[1]; } else { console.log("usage: topt(length) -- pt, px, mm, in units understood") } consoleLog(" topt",str,"==",result,"points"); return result; } //// pageArea() is a purely diagnostic/analytica function which prints the height of //// the page area and suggest modifications so that the page area will be a tight container for //// the prevailing line-height (which is an argument to the function) // e.g. pageArea('11pt'); // 1 in = 2.54 cm = 25.4 mm = 72 pt = 96 px = 6 pc // pt * 4 / 3 = px // pt / 28.346 = mm // 1pt = 0.352mm // 1mm = 2.84pt // 11pt = 3.872mm // 11.35pt = 3.995mm = 15.133px // 5mm = 14.2pt // 231pt = 81.3mm function pageArea(size) { schedule(pageArea_postlayout, size) } function pageArea_postlayout(lh) { var pages = PDF.pages; var delta; var lhpt = topt(lh); for (var i = 0; i element function alignCaptions() { var args = []; for (var i = 0; i < arguments.length; i++) { args.push(arguments[i]); consoleLog(" registering argument",arguments[i]); } schedule(aligncaption_postlayout, args); } function aligncaption_postlayout(args) { var selector; switch (args.length) { case 1: selector = args[0]; consoleLog("ac_postlayout: ",selector); break; case 2: selector = args[0]; tocselector = args[1]; exit; break; default: consoleLog("ac: wrong number of arguments") exit; } var elems = document.querySelectorAll(selector); for (var i = 0; i < elems.length; i++) { var child = elems[i]; var parent = elems[i].parentNode; var childboxes = elems[i].getPrinceBoxes(); if (childboxes.length > 0) { // we now know the height of the child box; next step is to find the height of the parent box and work out the difference var parentboxes = parent.getPrinceBoxes(); if (parentboxes.length > 0) { var diff = parentboxes[0].h - childboxes[0].h; // console.log("parent height is "+parentboxes[0].h+" vs child height "+childboxes[0].h+" diff is ",diff); child.style.marginTop = diff+"pt"; // console.log(" setting margintop to ",diff); } } } } //// add marker / call elements on sidenotes function addSidenoteMarks(selector, classname) { var elems = document.querySelectorAll(selector); consoleLog("addSidenoteMarks selecting "+selector+" -- adding class: "+classname); for (var i = 0; i < elems.length; i++) { var call = document.createElement("span"); call.classList.add(classname); consoleLog("addSidenoteMarks creaing "+call); elems[i].parentNode.insertBefore(call, elems[i]); } } //// toc function toc() { var toc_args = []; for (var i = 0; i < arguments.length; i++) { toc_args.push(arguments[i]); consoleLog(" toc registering argument",arguments[i]); } schedule(toc_postlayout, toc_args); } function toc_postlayout(toc_args) { var tocselector = "#toc"; switch (toc_args.length) { case 1: selector = toc_args[0]; consoleLog("toc_postlayout: ",selector); break; case 2: selector = toc_args[0]; tocselector = toc_args[1]; break; default: consoleLog("toc: wrong number of arguments") } // consoleLog("POSTLAYOUT These elements will appear in toc:",selector); // consoleLog("POSTLAYOUT ToC will appear inside this element:",tocselector); var elems = document.querySelectorAll(selector); var ToCelems = document.querySelectorAll(tocselector); if (ToCelems.length > 0) { var ToCelem = ToCelems[0]; } else { consoleLog("No ToC element found"); return; } for (var i = 0; i < elems.length; i++) { // var before = pseudotext(elems[i]); var str = addpseudotext(elems[i], " "," "); var text = document.createTextNode(str); // consoleLog("TEXT: ", str); // var text = document.createTextNode(getText(elems[i])); // var text = document.createTextNode(window.getComputedStyle(elems[i], ':before').content); elems[i].setAttribute("id", "ch"+i); var link = document.createElement("a"); link.setAttribute("href", "#ch"+i); link.appendChild(text); // link.appendChild(bt); var li = document.createElement("li"); li.className += elems[i].localName; li.appendChild(link); ToCelem.appendChild(li); } } function addpseudotext(el,bstr,astr) { var boxes = el.getPrinceBoxes(); // get the boxes associated with the paragraph, there can be more than one if the paragaph spans several pages var str = "" if (el.textContent) { str = el.textContent; } for (var j=0; j b.toLowerCase()) return 1; return 0;}); var el; // loop through entries and weed out references so that there is only one reference per entry per page. bold entries win over normal ones var prevmain = undefined; var main = undefined; var sub = undefined; for (var i=0; i= 0; j--) { // reverse order is important as we will cull the array along the way el = ix[a[j]]; var page = getPage(el); el.page = page; // record the page number of the element which caused the reference // consoleLog(" ref",j,el.id,el.className); if (page == prevpage) { // if we are still on the same page, something has to be deleted if (bold) { // if bold ref has been set for this page already, we should remove current ref a.splice(j,1); // remove ref // consoleLog(" removing in bold ",j); continue; } if (el.className == 'ixb') { // is this a bold page reference? if (normal) { // if normal ref has been set for this page, remove it a.splice(normal,1) // consoleLog(" removing in normal",j); } bold = j; // remember the bold reference continue; } if (normal) { // if this is a normal reference, remove the previous normal reference a.splice(normal,1) // consoleLog(" REMOVING IN NORMAL",j); normal = j; } } else { prevpage = page; if (el.className == 'ixb') { bold = j; // consoleLog(" recording bold",j); } else { normal = j; // consoleLog(" recording normal",j); } } } // we have now culled double entries, we now will go through the remaining entries and create the index. // the hard part in this section is to combine page numbers e.g. "1, 2, 3, 5" should become "1-3, 5" // re = /([^;]*);([^;]*)/; if (ent.search(/;/) > -1) { // is there a semi-colon in entry? var found = ent.match(/(.*)\s*;\s*(.*)/) var main = found[1]; var sub = found[2]; if (main == prevmain) { str = str + "\n
  • " + sub + " "; } else { prevmain = main; str = str + "\n
  • " + main + "\n
  • " + sub + " "; } } else { str = str + "\n
  • " + ent + " "; prevmain = ent; } var el=undefined; // this reference var pel=undefined; // the previous reference var series=undefined; // are we in a series that should be combined? for (var j=0; j < a.length; j++) { el = ix[a[j]]; // find element pointed to by ref el.page -=1; if (el.className) { // make string from element's classname var classname=" class="+el.className; } else { classname=""; } if (! pel) { // pel is undefined, so this is the first entry. str = str + "xx"; pel=el; continue; } if (pel.className) { // make string from previous element's classname var pclassname=" class="+pel.className; } else { pclassname=""; } if (el.page == pel.page + 1 ) { // if we are close if (el.className == pel.className) { series = "-"; // we are in a series which should be combined pel = el; continue; } else { // different classname, we must terminate previous element (pel) if (series) { str = str + "–"+pel.page+""; series = undefined; // series has been terminated } str = str + ", "+el.page+""; pel=el; } } else { // we are not close, so we must terminate previous element (pel) if (series) { str = str + "–"+pel.page+""; series = undefined; // series has been terminated } str = str + ", "+el.page+""; pel=el; } } if (series) { str = str + "–"+el.page+""; } } // consoleLog(str); document.getElementById("index").innerHTML = str; } function indexChapters(sel) { var ex = document.querySelectorAll(sel); for(var e=0; e b.toLowerCase()) return 1; return 0;}); var el; // loop through entries and weed out references so that there is only one reference per entry per page. bold entries win over normal ones var prevmain = undefined; var main = undefined; var sub = undefined; for (var i=0; i= 0; j--) { // reverse order is important as we will cull the array along the way el = ix[a[j]]; var page = getPage(el); el.page = page; // record the page number of the element which caused the reference // consoleLog(" ref",j,el.id,el.className); if (page == prevpage) { // if we are still on the same page, something has to be deleted if (bold) { // if bold ref has been set for this page already, we should remove current ref a.splice(j,1); // remove ref // consoleLog(" removing in bold ",j); continue; } if (el.className == 'ixb') { // is this a bold page reference? if (normal) { // if normal ref has been set for this page, remove it a.splice(normal,1) // consoleLog(" removing in normal",j); } bold = j; // remember the bold reference continue; } if (normal) { // if this is a normal reference, remove the previous normal reference a.splice(normal,1) // consoleLog(" REMOVING IN NORMAL",j); normal = j; } } else { prevpage = page; if (el.className == 'ixb') { bold = j; // consoleLog(" recording bold",j); } else { normal = j; // consoleLog(" recording normal",j); } } } // we have now culled double entries, we now will go through the remaining entries and create the index. // the hard part in this section is to combine page numbers e.g. "1, 2, 3, 5" should become "1-3, 5" // re = /([^;]*);([^;]*)/; if (ent.search(/;/) > -1) { // is there a semi-colon in entry? var found = ent.match(/(.*)\s*;\s*(.*)/) var main = found[1]; var sub = found[2]; if (main == prevmain) { str = str + "\n
  • " + sub + " "; } else { prevmain = main; str = str + "\n
  • " + main + "\n
  • " + sub + " "; } } else { str = str + "\n
  • " + ent + " "; prevmain = ent; } var el=undefined; // this reference var pel=undefined; // the previous reference var series=undefined; // are we in a series that should be combined? for (var j=0; j < a.length; j++) { el = ix[a[j]]; // find element pointed to by ref el.page -=1; if (el.className) { // make string from element's classname var classname=" class="+el.className; } else { classname=""; } if (! pel) { // pel is undefined, so this is the first entry. str = str + "xx"; pel=el; continue; } if (pel.className) { // make string from previous element's classname var pclassname=" class="+pel.className; } else { pclassname=""; } if (el.page == pel.page + 1 ) { // if we are close if (el.className == pel.className) { series = "-"; // we are in a series which should be combined pel = el; continue; } else { // different classname, we must terminate previous element (pel) if (series) { str = str + "–"+pel.page+""; series = undefined; // series has been terminated } str = str + ", "+el.page+""; pel=el; } } else { // we are not close, so we must terminate previous element (pel) if (series) { str = str + "–"+pel.page+""; series = undefined; // series has been terminated } str = str + ", "+el.page+""; pel=el; } } if (series) { str = str + "–"+el.page+""; } } // consoleLog(str); document.getElementById("index").innerHTML = str; } function getPage(el) { var elboxes = el.getPrinceBoxes(); if (elboxes.length > 0) { return elboxes[0].pageNum; // page number where ref appears } else { consoleLog("GetPage, can't find page"); return udefined; } } //// simpletoc function simpleToc() { var selector; var tocselector = "#toc"; switch (arguments.length) { case 1: selector = arguments[0]; break; case 2: selector = arguments[0]; tocselector = arguments[1]; break; default: consoleLog("toc: wrong number of arguments") } // consoleLog("SIMPLETOC These elements will appear in toc:",selector); // consoleLog("SIMPLETOC ToC will appear inside this element:",tocselector); var elems = document.querySelectorAll(selector); var ToCelems = document.querySelectorAll(tocselector); if (ToCelems.length > 0) { var ToCelem = ToCelems[0]; } else { consoleLog("No ToC element found"); exit; } for (var i = 0; i < elems.length; i++) { var str = elems[i].textContent; var text = document.createTextNode(str); // consoleLog("TEXT: ", str); // var text = document.createTextNode(getText(elems[i])); // var text = document.createTextNode(window.getComputedStyle(elems[i], ':before').content); elems[i].setAttribute("id", "ch"+i); var link = document.createElement("a"); link.setAttribute("href", "#ch"+i); link.appendChild(text); // link.appendChild(bt); var li = document.createElement("li"); li.className += elems[i].localName; li.appendChild(link); ToCelem.appendChild(li); } } function boxInfo(selector) { console.log("boxInfo",selector); schedule(boxInfo_postlayout,selector); } function boxInfo_postlayout(selector) { var e = document.querySelectorAll(selector); // console.log("number of elements",e.length, selector); var b = e[0].getPrinceBoxes(); // printObject(b[0]); // console.log("Content width is " + b[0].contentBox("cm").w + "cm"); return b[0]; } // pageInfo function pageInfo(querypage) { // console.log("pageInfo",querypage); schedule(pageInfo_postlayout,querypage); } function pageInfo_postlayout(querypage) { var pages = PDF.pages; console.log("\n"); console.log("
    \n");
       printObject(PDF.pages[10]);
       console.log("
    \n"); if (querypage) { if (querypage <= pages.length) { printBoxes(" ",pages[querypage-1]); } } else { for(var i=0; i"); console.log(box.type); console.log(""); for (var i=0; i"); } // is there footnotes area on this page? // if so, record its size, establish the bounds // look for notes on the page (based on a selector) -- two selectors: fixed notes, and flexible notes // loop through all notes -- do they collide? // if so, go through from top to bottom. Push movaledownwards. // if still conflict, move upwards function findFootnoteBox(box) { // console.log ("findfnbox",box.type); if (box.type == "FOOTNOTES") { return(box); } for (var i=0; i // // The following methods will then be available on BoxInfo objects returned // by getPrinceBoxes(). // // Methods: // marginBox(u) // borderBox(u) // paddingBox(u) // contentBox(u) // // Parameters: // u The units to use. One of "cm", "in", "mm", "q", "pc", "pt" or "px". // // Return value: // A box object with properties x, y, w and h giving the position and // size of the box in the requested units. // // Example: // var e = document.getElementById("someId"); // var b = e.getPrinceBoxes()[0]; // console.log("Content width is " + b.contentBox("cm").w + "cm"); function Box(x, y, w, h, u) { var d = 1; if (u == "cm") { d = 72/2.54; } else if (u == "in") { d = 72; } else if (u == "mm") { d = 72/25.4; } else if (u == "q") { d = 72/101.6; } else if (u == "pc") { d = 1/12; } else if (u == "pt") { d = 1; } else if (u == "px") { d = 72/96; } else { console.log("Box: unknown units: " + u); } this.x = x/d; this.y = y/d; this.w = w/d; this.h = h/d; } function addBoxInfoMethods() { BoxInfo.prototype.marginBox = function (u) { var x = this.x - this.marginLeft; var y = this.y + this.marginTop; var w = this.w + this.marginLeft + this.marginRight; var h = this.h + this.marginTop + this.marginBottom; return new Box(x, y, w, h, u); }; BoxInfo.prototype.borderBox = function (u) { var x = this.x; var y = this.y; var w = this.w; var h = this.h; return new Box(x, y, w, h, u); }; BoxInfo.prototype.paddingBox = function(u) { var bTop = parseFloat(this.style.borderTopWidth.slice(0, -2)); var bRight = parseFloat(this.style.borderRightWidth.slice(0, -2)); var bBottom = parseFloat(this.style.borderBottomWidth.slice(0, -2)); var bLeft = parseFloat(this.style.borderLeftWidth.slice(0, -2)); var x = this.x + bLeft; var y = this.y - bTop; var w = this.w - bLeft - bRight; var h = this.h - bTop - bBottom; return new Box(x, y, w, h, u); }; BoxInfo.prototype.contentBox = function(u) { var bTop = parseFloat(this.style.borderTopWidth.slice(0, -2)); var bRight = parseFloat(this.style.borderRightWidth.slice(0, -2)); var bBottom = parseFloat(this.style.borderBottomWidth.slice(0, -2)); var bLeft = parseFloat(this.style.borderLeftWidth.slice(0, -2)); var pTop = parseFloat(this.style.paddingTop.slice(0, -2)); var pRight = parseFloat(this.style.paddingRight.slice(0, -2)); var pBottom = parseFloat(this.style.paddingBottom.slice(0, -2)); var pLeft = parseFloat(this.style.paddingLeft.slice(0, -2)); var x = this.x + bLeft + pLeft; var y = this.y - bTop - pTop; var w = this.w - bLeft - bRight - pLeft - pRight; var h = this.h - bTop - bBottom - pTop - pBottom; return new Box(x, y, w, h, u); }; } /* addToggle adds one