3 # Part of Objavi2, which turns html manuals into books
5 # Copyright (C) 2009 Douglas Bagnall
7 # This program is free software; you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 2 of the License, or
10 # (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License along
18 # with this program; if not, write to the Free Software Foundation, Inc.,
19 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
21 """Make a pdf from the specified book."""
22 from __future__
import with_statement
26 from pprint
import pformat
28 from objavi
.fmbook
import log
, Book
, ZipBook
, make_book_name
29 from objavi
import config
30 from objavi
.cgi_utils
import parse_args
, optionise
, listify
, shift_file
31 from objavi
.cgi_utils
import output_blob_and_exit
33 from objavi
.twiki_wrapper
import get_book_list
35 FORM_TEMPLATE
= os
.path
.abspath('templates/form.html')
36 PROGRESS_TEMPLATE
= os
.path
.abspath('templates/progress.html')
39 #spaces?, digits!, dot?, digits?, spaces?
40 #return re.compile(r'^\s*[+-]?\d+\.?\d*\s*$').match
47 def isfloat_or_auto(s
):
48 return isfloat(s
) or s
.lower() in ('', 'auto')
51 # 10 or 13 digits with any number of hyphens, perhaps with check-digit missing
53 return (re
.match(r
'^\d+[\dXx*]$', s
) and len(s
) in (9, 10, 12, 13))
56 # ARG_VALIDATORS is a mapping between the expected cgi arguments and
57 # functions to validate their values. (None means no validation).
59 "book": re
.compile(r
'^(\w+/?)*\w+$').match
, # can be: BlahBlah/Blah_Blah
60 "css": None, # an url, empty (for default), or css content
61 "title": lambda x
: len(x
) < 999,
62 #"header": None, # header text, UNUSED
64 "license": config
.LICENSES
.__contains
__,
65 "server": config
.SERVER_DEFAULTS
.__contains
__,
66 "engine": config
.ENGINES
.__contains
__,
67 "booksize": config
.PAGE_SIZE_DATA
.__contains
__,
68 "page_width": isfloat
,
69 "page_height": isfloat
,
70 "gutter": isfloat_or_auto
,
71 "top_margin": isfloat_or_auto
,
72 "side_margin": isfloat_or_auto
,
73 "bottom_margin": isfloat_or_auto
,
74 "columns": isfloat_or_auto
,
75 "column_margin": isfloat_or_auto
,
76 "cgi-context": lambda x
: x
.lower() in '1true0false',
77 "mode": config
.CGI_MODES
.__contains
__,
78 "pdftype": lambda x
: config
.CGI_MODES
.get(x
, [False])[0],
79 "rotate": u
"yes".__eq
__,
80 "grey_scale": u
"yes".__eq
__,
81 "destination": config
.EPUB_DESTINATIONS
.__contains
__,
84 __doc__
+= '\nValid arguments are: %s.\n' % ', '.join(ARG_VALIDATORS
.keys())
87 def get_server_list():
88 return sorted(k
for k
, v
in config
.SERVER_DEFAULTS
.items() if v
['display'])
92 #order by increasing areal size.
93 def calc_size(name
, pointsize
, klass
):
95 mmx
= pointsize
[0] * config
.POINT_2_MM
96 mmy
= pointsize
[1] * config
.POINT_2_MM
97 return (mmx
* mmy
, name
, klass
,
98 '%s (%dmm x %dmm)' % (name
, mmx
, mmy
))
100 return (0, name
, klass
, name
) # presumably 'custom'
102 return [x
[1:] for x
in sorted(calc_size(k
, v
.get('pointsize'), v
.get('class', ''))
103 for k
, v
in config
.PAGE_SIZE_DATA
.iteritems())
106 def get_default_css(server
=config
.DEFAULT_SERVER
, mode
='book'):
107 """Get the default CSS text for the selected server"""
109 cssfile
= config
.SERVER_DEFAULTS
[server
]['css-%s' % mode
]
117 """Links to various example pdfs."""
119 for script
in os
.listdir(config
.FONT_EXAMPLE_SCRIPT_DIR
):
120 if not script
.isalnum():
121 log("warning: font-sample %s won't work; skipping" % script
)
123 links
.append('<a href="%s?script=%s">%s</a>' % (config
.FONT_LIST_URL
, script
, script
))
127 def make_progress_page(book
, bookname
, mode
):
128 """Return a function that will notify the user of progress. In
129 CGI context this means making an html page to display the
130 messages, which are then sent as javascript snippets on the same
133 return lambda message
: '******* got message "%s"' %message
135 f
= open(PROGRESS_TEMPLATE
)
138 progress_list
= ''.join('<li id="%s">%s</li>\n' % x
[:2] for x
in config
.PROGRESS_POINTS
143 'bookname': bookname
,
144 'progress_list': progress_list
,
147 def progress_notifier(message
):
149 print ('<script type="text/javascript">\n'
150 'objavi_show_progress("%s");\n'
151 '</script>' % message
153 if message
== 'finished':
154 print '</body></html>'
156 except ValueError, e
:
157 log("failed to send message %r, got exception %r" % (message
, e
))
158 return progress_notifier
161 def get_page_settings(args
):
162 """Find the size and any optional layout settings.
164 args['booksize'] is either a keyword describing a size or
165 'custom'. If it is custom, the form is inspected for specific
166 dimensions -- otherwise these are ignored.
168 The margins, gutter, number of columns, and column
169 margins all set themselves automatically based on the page
170 dimensions, but they can be overridden. Any that are are
172 # get all the values including sizes first
173 # the sizes are found as 'page_width' and 'page_height',
174 # but the Book class expects them as a 'pointsize' tuple, so
175 # they are easily ignored.
177 for k
, extrema
in config
.PAGE_EXTREMA
.iteritems():
179 v
= float(args
.get(k
))
180 except (ValueError, TypeError):
181 #log("don't like %r as a float value for %s!" % (args.get(k), k))
183 min_val
, max_val
, multiplier
= extrema
184 if v
< min_val
or v
> max_val
:
185 log('rejecting %s: outside %s' % (v
,) + extrema
)
187 log('found %s=%s' % (k
, v
))
188 settings
[k
] = v
* multiplier
#convert to points in many cases
190 # now if args['size'] is not 'custom', the width and height found
192 size
= args
.get('booksize', config
.DEFAULT_SIZE
)
193 settings
.update(config
.PAGE_SIZE_DATA
[size
])
195 #if args['mode'] is 'newspaper', then the number of columns is
196 #automatically determined unless set -- otherwise default is 1.
197 if args
.get('mode') == 'newspaper' and settings
.get('columns') is None:
198 settings
['columns'] = 'auto'
200 if args
.get('grey_scale'):
201 settings
['grey_scale'] = True
204 #will raise KeyError if width, height aren't set
205 settings
['pointsize'] = (settings
['page_width'], settings
['page_height'])
206 del settings
['page_width']
207 del settings
['page_height']
209 settings
['engine'] = args
.get('engine', config
.DEFAULT_ENGINE
)
213 def output_and_exit(f
):
214 """Decorator: prefix function output with http headers and exit
215 immediately after."""
218 print "Content-type: text/html; charset=utf-8\n"
224 def mode_booklist(args
):
225 print optionise(get_book_list(args
.get('server', config
.DEFAULT_SERVER
)),
226 default
=args
.get('book'))
230 #XX sending as text/html, but it doesn't really matter
231 print get_default_css(args
.get('server', config
.DEFAULT_SERVER
), args
.get('pdftype', 'book'))
236 f
= open(FORM_TEMPLATE
)
239 f
= open(config
.FONT_LIST_INCLUDE
)
240 font_list
= [x
.strip() for x
in f
if x
.strip()]
242 server
= args
.get('server', config
.DEFAULT_SERVER
)
243 book
= args
.get('book')
244 size
= args
.get('booksize', config
.DEFAULT_SIZE
)
245 engine
= args
.get('engine', config
.DEFAULT_ENGINE
)
247 'server_options': optionise(get_server_list(), default
=server
),
248 'book_options': optionise(get_book_list(server
), default
=book
),
249 'size_options': optionise(get_size_list(), default
=size
),
250 'engines': optionise(config
.ENGINES
.keys(), default
=engine
),
251 'pdf_types': optionise(sorted(k
for k
, v
in config
.CGI_MODES
.iteritems() if v
[0])),
252 'css': get_default_css(server
),
253 'font_links': listify(font_links()),
254 'font_list': listify(font_list
),
255 'default_license' : config
.DEFAULT_LICENSE
,
256 'licenses' : optionise(config
.LICENSES
, default
=config
.DEFAULT_LICENSE
),
262 for id, title
, type, source
, classes
, epilogue
in config
.FORM_INPUTS
:
263 val
= d
.get(source
, '')
264 e
= config
.FORM_ELEMENT_TYPES
[type] % locals()
265 form
.append('\n<div id="%(id)s_div" class="form-item %(classes)s">\n'
266 '<div class="input_title">%(title)s</div>\n'
267 '<div class="input_contents"> %(e)s %(epilogue)s\n</div>'
268 '</div>\n' % locals())
271 _valid_inputs
= set(ARG_VALIDATORS
)
272 _form_inputs
= set(x
[0] for x
in config
.FORM_INPUTS
if x
[2] != 'ul')
273 log("valid but not used inputs: %s" % (_valid_inputs
- _form_inputs
))
274 log("invalid form inputs: %s" % (_form_inputs
- _valid_inputs
))
276 print template
% {'form': ''.join(form
)}
281 # so we're making a pdf.
282 mode
= args
.get('mode', 'book')
283 bookid
= args
.get('book')
284 server
= args
.get('server', config
.DEFAULT_SERVER
)
285 page_settings
= get_page_settings(args
)
286 bookname
= make_book_name(bookid
, server
)
287 progress_bar
= make_progress_page(bookid
, bookname
, mode
)
289 with
Book(bookid
, server
, bookname
, page_settings
=page_settings
,
290 watcher
=progress_bar
, isbn
=args
.get('isbn'),
291 license
=args
.get('license')) as book
:
295 book
.set_title(args
.get('title'))
296 book
.add_css(args
.get('css'), mode
)
297 book
.add_section_titles()
301 elif mode
in ('web', 'newspaper'):
302 book
.make_simple_pdf(mode
)
307 book
.notify_watcher('finished')
309 #These ones are similar enough to be handled by the one function
310 mode_newspaper
= mode_book
315 def mode_openoffice(args
):
316 """Make an openoffice document. A whole lot of the inputs have no
318 bookid
= args
.get('book')
319 server
= args
.get('server', config
.DEFAULT_SERVER
)
320 #page_settings = get_page_settings(args)
321 bookname
= make_book_name(bookid
, server
, '.odt')
322 progress_bar
= make_progress_page(bookid
, bookname
, 'openoffice')
324 with
Book(bookid
, server
, bookname
,
325 watcher
=progress_bar
, isbn
=args
.get('isbn'),
326 license
=args
.get('license')) as book
:
330 book
.set_title(args
.get('title'))
331 book
.add_css(args
.get('css'), 'openoffice')
332 book
.add_section_titles()
334 book
.notify_watcher('finished')
336 #Not using output_and_exit, because the content type might not be text/html
338 log('making epub with\n%s' % pformat(args
))
339 bookid
= args
.get('book')
340 #server = args.get('server', config.BOOKI_SERVER)
341 server
= args
.get('server', config
.DEFAULT_SERVER
)
342 destination
= args
.get('destination', config
.DEFAULT_EPUB_DESTINATION
)
343 bookname
= '%s.epub' % (bookid
,)
345 if destination
== 'html':
346 print 'content-type: text/html\n'
347 progress_bar
= make_progress_page(bookid
, bookname
, 'epub')
351 book
= ZipBook(server
, bookid
, watcher
=progress_bar
)
352 book
.make_epub(use_cache
=config
.USE_CACHED_IMAGES
)
353 fn
= shift_file(book
.epubfile
, config
.EPUB_DIR
)
355 if destination
== 'html':
356 book
.notify_watcher('finished')
357 elif destination
== 'download':
361 output_blob_and_exit(data
, 'application/epub+zip', bookname
)
362 elif destination
== 'archive.org':
368 args
= parse_args(ARG_VALIDATORS
)
369 mode
= args
.get('mode')
370 if mode
is None and 'book' in args
:
374 CGI_CONTEXT
= 'SERVER_NAME' in os
.environ
or args
.get('cgi-context', 'no').lower() in '1true'
376 if not args
and not CGI_CONTEXT
:
380 output_function
= globals().get('mode_%s' % mode
, mode_form
)
381 output_function(args
)
383 if __name__
== '__main__':
384 if config
.CGITB_DOMAINS
and os
.environ
.get('REMOTE_ADDR') in config
.CGITB_DOMAINS
: