alias "source" to "server" in booki-twiki-gateway.cgi, to match espri
[objavi2.git] / libobjavi.qs
blobca762640db666001ef16ebcddee931dbf4ab2a21
1 /*
2 Part of Objavi2, which makes pdf versions of FLOSSManuals books.
3 This is a QSA script library for pdfedit which provides various page
4 manipulation routines.
6 Copyright (C) 2009 Douglas Bagnall
8 This program is free software; you can redistribute it and/or modify
9 it under the terms of the GNU General Public License as published by
10 the Free Software Foundation; either version 2 of the License, or (at
11 your option) any later version.
13 This program is distributed in the hope that it will be useful,
14 but WITHOUT ANY WARRANTY; without even the implied warranty of
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 GNU General Public License for more details.
18 You should have received a copy of the GNU General Public License along
19 with this program; if not, write to the Free Software Foundation, Inc.,
20 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
24  * Shift pages alternately left or right, and add page numbers in the
25  * outer corners.
26  *
27  * This is a QSA script for pdfedit. QSA is a (deprecated) dialect of
28  * ecmascript used for scripting QT applications like pdfedit.
29  *
30  */
32 const DEFAULT_OFFSET = 25;
33 /* FLOSS Manuals default book size at lulu: 'Comicbook', 6.625" x 10.25" */
34 const COMIC_WIDTH = (6.625 * 72);
35 const COMIC_HEIGHT = (10.25 * 72);
37 const PAGE_NUMBER_SIZE = 11;
39 const DEFAULT_DIR = 'LTR';
41 const DEFAULT_NUMBER_STYLE = 'latin';
42 const DEFAULT_ENGINE = 'webkit';
44 function transform_page(page, offset){
45     page.setTransformMatrix([1, 0, 0, 1, offset, 0]);
48 function rotate_page180(page){
49     /* From the PDF reference:
50      *
51      * Rotations are produced by
52      *  [cos(theta) sin(theta) −sin(theta) cos(theta) 0 0],
53      * which has the effect of rotating the coordinate
54      * system axes by an angle theta counterclockwise.
55      *
56      * but the rotation is about the 0,0 axis: ie the top left.
57      * So addin the width and height is necessary.
58      *
59      * There is *also* a Rotate key in the page or document dictionary
60      * that can be used to rotate in multiples of 90 degrees. But I did
61      * not find that first, and it fails mysteriously in pdfedit.
62      *
63      *     var d = page.getDictionary();
64      *     d.add('Rotate', createInt(180));
65      *
66      */
67     var box = page.mediabox();
68     //print("box is " + box);
70     angle = Math.PI ;
71     var c = Math.cos(angle);
72     var s = Math.sin(angle);
73     page.setTransformMatrix([c, s, -s, c, box[2], box[3]]);
76 function adjust_for_direction(pdf, dir){
77     /* RTL book have gutter on the other side */
78     /*rotate the file if RTL*/
79     if (dir == 'RTL'){
80         //offset = -offset;
81         process_pdf(pdf, rotate_page180);
82     }
85 function  prepend_content(page, content, prepended_op){
86     /* Webkit applies a transformation matrix so it can write the pdf
87      * using its native axes.  That means, to use the default grid we
88      * need to either insert the page number before the webkit matrix,
89      * or apply an inverse.  The inverse looks like:
90      *
91      * createOperator("cm", iprop_array('nnnnnn', 16.66667, 0, 0, -16.66667, -709.01015, 11344.83908));
92      *
93      * but it is simpler to jump in first.
94      *
95      * XXX warning: the order in which these things are applied can matter.
96      */
97     if (prepended_op == undefined)
98         prepended_op = 'cm';
100     var stream = page.getContentStream(0);
101     var iter = stream.getFirstOperator().iterator();
102     var op;
103     do {
104         op = iter.current();
105         if (op.getName() == prepended_op){
106             iter.prev();
107             op = iter.current();
108             break;
109         }
110     } while (iter.next());
112     stream.insertOperator(op, content);
116 function grow_and_shift_page(page, data){
117     //get the box size in order to recentre (i.e. add half width & height delta)
118     var box = page.mediabox();
119     var x = box[0];
120     var y = box[1];
121     var w = box[2] - box[0];
122     var h = box[3] - box[1];
124     page.setMediabox(x, y, x + data.width, y + data.height);
126     var dx = data.offset + (width - w) * 0.5;
127     var dy = (height - h) * 0.5;
129     var cm = createOperator("cm", iprop_array('nnnnnn', 1, 0, 0, 1, dx, dy));
130     prepend_content(page, cm, 'q');
135 function shift_page_mediabox(page, data){
136     var box = page.mediabox();
137     var x = box[0];
138     var y = box[1];
139     var w = box[2] - box[0];
140     var h = box[3] - box[1];
142     if (data.width || data.height){
143         /*resize each page, so put the mediabox out and up by half of
144         the difference.
145          XXX should the page really be centred vertically? */
146         x -= 0.5 * (data.width - w);
147         y -= 0.5 * (data.height - h);
148         w = data.width;
149         h = data.height;
150         //print("now x, y = " + x + ", " + y);
151     }
152     page.setMediabox(x - data.offset, y, x + w - data.offset, y + h);
155 function add_transformation(page, data){
156     var t = data.transform;
157     var cm = createOperator("cm", iprop_array('nnnnnn', t[0], t[1], t[2], t[3], t[4], t[5], t[6]));
163  * Helper functions for creating pdf operators. You can create an
164  * IPropertyArray or a PdfOperator like this:
166  * var array = iprop_array("Nnns", "Name", 1, 1.2, "string"); var rg =
167  * operator('rg', "nnn", 0, 1, 0);
169  * where the first argument to operator and the second to iprop_array
170  * is a template (ala Python's C interface) that determines the
171  * interpretation of subsequent parameters. Common keys are 'n' for
172  * (floaing point) number, 's' for string, 'N' for name, 'i' for
173  * integer. See the convertors object below.
175  */
177 var convertors = {
178     a: createArray,
179     b: createBool,
180     //c: createCompositeOperator,
181     d: createDict,
182     o: createEmptyOperator,
183     i: createInt,
184     N: createName,
185     O: createOperator,
186     n: createReal,
187     r: createRef,
188     s: createString,
189     z: function(x){ return x; } //for pre-existing object, no processing.
192 function iprop_array(pattern){
193     //print("doing " + arguments);
194     var array = createIPropertyArray();
195     for (i = 1; i < arguments.length; i++){
196         var s = pattern.charAt(i - 1);
197         var arg = arguments[i];
198         var op = convertors[s](arg);
199         array.append(op);
200     }
201     return array;
205 /* roman_number(number) -> lowercase roman number + approximate width */
206 function roman_number(number){
207     var data = [
208         'm', 1000,
209         'd', 500,
210         'c', 100,
211         'l', 50,
212         'x', 10,
213         'v', 5,
214         'i', 1
215     ];
217     var i;
218     var s = '';
219     for (i = 0; i < data.length; i += 2){
220         var value = data[i + 1];
221         var letter = data[i];
222         while (number >= value){
223             s += letter;
224             number -= value;
225         }
226     }
228     var subs = [
229         'dcccc', 'cm',
230         'cccc', 'cd',
231         'lxxxx', 'xc',
232         'xxxx', 'xl',
233         'viiii', 'ix',
234         'iiii', 'iv'
235     ];
236     for (i = 0; i < subs.length; i+= 2){
237         s = s.replace(subs[i], subs[i + 1]);
238     }
240     /* Try to take into account variable widths.
241      * XXX these numbers are made up, and font-specific.
242      */
243     var widths = {
244         m: 0.9,
245         d: 0.6,
246         c: 0.6,
247         l: 0.3,
248         x: 0.6,
249         v: 0.6,
250         i: 0.3
251     };
253     var w = 0;
254     for (i = 0; i < s.length; i++){
255         w += widths[s.charAt(i)];
256     }
258     return {
259         text: s,
260         width: w
261     };
265 /* change_number_charset(number, charset) -> unicode numeral + approx. width
266  farsi =  '۰۱۲۳۴۵۶۷۸۹';
267  arabic = '٠١٢٣٤٥٦٧٨٩';
268  */
269 function change_number_charset(number, charset){
270     var charsets = {
271         arabic: ['٠', '١', '٢', '٣', '٤', '٥', '٦', '٧', '٨', '٩'],
272         farsi:  ['۰', '۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹']
273     };
274     if (charset == undefined)
275         charset = 'arabic';
276     var numerals = charsets[charset];
277     var west = number.toString();
278     var i;
279     var s = '';
280     for (i = 0; i < west.length; i++){
281         var c = west.charAt(i);
282         s += numerals[parseInt(c)];
283     }
284     //lazy guess at width
285     var w = 0.6 * s.length;
286     return {
287         text: s,
288         width: w
289     };
292 function latin_number(number){
293     var text = number.toString();
294     /* It would be nice to know the bounding box of the page number,
295      but it is not rendered during this process, so we have to guess.
296      All Helvetica numerals are the same width (approximately N shape)
297      so I'll assume 0.6ish.
298      */
299     return {
300         text: text,
301         width: text.length * 0.6
302     };
305 var stringifiers = {
306     roman:  roman_number,
307     arabic: change_number_charset,
308     farsi:  change_number_charset,
309     latin:  latin_number
312 function add_page_number(page, number, dir, style, margin, bottom,
313                          preceding_operators){
314     //print(' ' + number + ' ' +  dir + ' ' + style);
315     if (! style in stringifiers || style == undefined){
316         style = 'latin';
317     }
318     var box = page.mediabox();
319     var h = PAGE_NUMBER_SIZE;
320     var n = stringifiers[style](number, style);
321     var text = n.text;
322     var w = n.width * h;
324     var y = box[1] + bottom;
325     var x = box[0] + margin;
327     if ((number & 1) == (dir != 'RTL')){
328         x = box[2] - margin - w;
329     }
331     var q = createCompositeOperator("q", "Q");
332     var BT = createCompositeOperator("BT", "ET");
334     /* it would be nice to use the book's font, but it seems not to
335     work for unknown reasons.  */
336     page.addSystemType1Font("Helvetica");
337     var font = page.getFontId("Helvetica"); // probably 'PDFEDIT_F1'
339     var rg = createOperator("rg", iprop_array('nnn', 0, 0, 0));
340     var tf = createOperator("Tf", iprop_array('Nn', font, h));
341     var td = createOperator("Td", iprop_array('nn', x, y));
342     var tj = createOperator("Tj", iprop_array('s', text));
343     var et = createOperator("ET", iprop_array());
344     var end_q = createOperator("Q", iprop_array());
346     BT.pushBack(rg, BT);
347     BT.pushBack(tf, rg);
348     BT.pushBack(td, tf);
349     BT.pushBack(tj, td);
350     BT.pushBack(et, tj);
352     /*If given extra operators, push them in first */
353     if (0 && preceding_operators != undefined){
354         print('' + preceding_operators[0]);
355         var i;
356         for (i = 0; i < preceding_operators.length; i++){
357             q.pushBack(preceding_operators[i], q);
358         }
359     }
361     q.pushBack(BT, q);
362     q.pushBack(end_q, BT);
364     var ops = createPdfOperatorStack();
365     ops.append(q);
366     page.appendContentStream(ops);
371 function number_pdf_pages(pdf, dir, number_style, start,
372                           margin, bottom,
373                           preceding_operators){
374     var pages = pdf.getPageCount();
375     var i;
376     var offset = 0;
377     print("numbers start at " + start + "; offset is " + offset + "sum = " + (start + offset));
378     start = parseInt(start) || 1;
379     if (start < 0){
380         /*count down (-start) pages before beginning */
381         offset = -start;
382         start = 1;
383     }
384     else {
385         /* start numbering at (start) */
386         offset = 1 - start;
387     }
388     for (i = start; i <= pages - offset; i++){
389         add_page_number(pdf.getPage(i + offset), i, dir, number_style,
390                         margin, bottom, preceding_operators);
391     }
395 function process_pdf(pdf, func, data, skip_start, skip_end){
396     var pages = pdf.getPageCount();
397     var i = 1;
398     if (! isNaN(skip_start)){
399         print ("skipping " + skip_start + "pages");
400         i += skip_start;
401         if (skip_start & 1)
402             data[data.flip] = -data[data.flip];
403         print (data.flip + " is  now " + data[data.flip]);
404     }
405     if (! isNaN(skip_end)){
406         pages -= skip_end;
407     }
408     for (; i <= pages; i++){
409         func(pdf.getPage(i), data);
410         if (data != undefined && data.flip)
411             data[data.flip] = -data[data.flip];
412     }
415 function even_pages(pdf){
416     /* if the pdf has an odd number of pages, cut one off the end.
417      * The pdf generator should have made an extra page in case this is necessary.
418      */
420     var pages = pdf.getPageCount();
421     if (pages & 1){
422         pdf.removePage(pages); //one-based numbering
423     }
427 function save_text_index(pdf, filename){
428     //create an index file for finding chapter page numbers
429     var pages = pdf.getPageCount();
430     //open file
431     var outfile = new File(filename);
432     outfile.open(File.WriteOnly);
434     var write = function(x){
435         outfile.write(x + '\n');
436     };
438     for (var i = 1; i <= pages; i++){
439         write('\n-=-=- Magic Page Separating Line Not Found In Any Books -=-=-');
440         write(i);
441         write((pdf.getPage(i).getText()));
442     }
444     outfile.close();
451 function parse_options(parameters, options, convertors){
452     /* split parameters on the first '=' and return a mapping object.
453      * a mapping of default options can be passed in. */
454     if (options == undefined)
455         options = {};
456     if (convertors == undefined)
457         convertors = {};
459     print(parameters + '');
460     var i;
461     for (i = 0; i < parameters.length; i++){
462         var p = parameters[i];
463         if (p == 'h' || p == 'help')
464             commandline_help(options);
466         var split = p.indexOf('=');
467         var key = p.substring(0, split);
468         var value = p.substring(split + 1);
469         if (key in convertors)
470             options[key] = convertors[key](value);
471         else
472             options[key] = value;
473     }
474     return options;
477 function commandline_help(options){
478     print("options are:");
479     var padding = "                   ";
480     for (var o in options){
481         print(o + padding.substring(0, padding.length - o.length) + '[' + options[o] + ']');
482     }
483     exit(0);