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 sys
.path
.insert(0, os
.path
.abspath('.'))
27 #print >> sys.stderr, sys.path
30 from pprint
import pformat
32 from objavi
.fmbook
import log
, Book
, make_book_name
, HTTP_HOST
33 from objavi
import config
34 from objavi
.cgi_utils
import parse_args
, optionise
, listify
, output_and_exit
, font_links
, get_default_css
35 from objavi
.cgi_utils
import output_blob_and_exit
, is_utf8
, isfloat
, isfloat_or_auto
, is_isbn
36 from objavi
.twiki_wrapper
import get_book_list
39 # ARG_VALIDATORS is a mapping between the expected cgi arguments and
40 # functions to validate their values. (None means no validation).
42 "book": re
.compile(r
'^([\w-]+/?)*[\w-]+$').match
, # can be: BlahBlah/Blah_Blah
43 "css": is_utf8
, # an url, empty (for default), or css content
44 "title": lambda x
: len(x
) < 999 and is_utf8(x
),
45 #"header": None, # header text, UNUSED
47 "license": config
.LICENSES
.__contains
__,
48 "server": config
.SERVER_DEFAULTS
.__contains
__,
49 "engine": config
.ENGINES
.__contains
__,
50 "booksize": config
.PAGE_SIZE_DATA
.__contains
__,
51 "page_width": isfloat
,
52 "page_height": isfloat
,
53 "gutter": isfloat_or_auto
,
54 "top_margin": isfloat_or_auto
,
55 "side_margin": isfloat_or_auto
,
56 "bottom_margin": isfloat_or_auto
,
57 "columns": isfloat_or_auto
,
58 "column_margin": isfloat_or_auto
,
59 "cgi-context": lambda x
: x
.lower() in '1true0false',
60 "mode": config
.CGI_MODES
.__contains
__,
61 "pdftype": lambda x
: config
.CGI_MODES
.get(x
, [False])[0],
62 "rotate": u
"yes".__eq
__,
63 "grey_scale": u
"yes".__eq
__,
64 "destination": config
.CGI_DESTINATIONS
.__contains
__,
65 "toc_header": is_utf8
,
69 __doc__
+= '\nValid arguments are: %s.\n' % ', '.join(ARG_VALIDATORS
.keys())
72 def get_server_list():
73 return sorted(k
for k
, v
in config
.SERVER_DEFAULTS
.items() if v
['display'])
77 #order by increasing areal size.
78 def calc_size(name
, pointsize
, klass
):
80 mmx
= pointsize
[0] * config
.POINT_2_MM
81 mmy
= pointsize
[1] * config
.POINT_2_MM
82 return (mmx
* mmy
, name
, klass
,
83 '%s (%dmm x %dmm)' % (name
, mmx
, mmy
))
85 return (0, name
, klass
, name
) # presumably 'custom'
87 return [x
[1:] for x
in sorted(calc_size(k
, v
.get('pointsize'), v
.get('class', ''))
88 for k
, v
in config
.PAGE_SIZE_DATA
.iteritems())
91 def make_progress_page(book
, bookname
, mode
, destination
='html'):
92 """Return a function that will notify the user of progress. In
93 CGI context this means making an html page to display the
94 messages, which are then sent as javascript snippets on the same
96 if not CGI_CONTEXT
or destination
!= 'html':
97 return lambda message
: '******* got message "%s"' %message
99 print "Content-type: text/html; charset=utf-8\n"
100 f
= open(config
.PROGRESS_TEMPLATE
)
103 progress_list
= ''.join('<li id="%s">%s</li>\n' % x
[:2] for x
in config
.PROGRESS_POINTS
108 'bookname': bookname
,
109 'progress_list': progress_list
,
112 def progress_notifier(message
):
114 if message
.startswith('ERROR:'):
115 log('got an error! %r' % message
)
116 print ('<b class="error-message">'
118 '</b></body></html>' % message
121 print ('<script type="text/javascript">\n'
122 'objavi_show_progress("%s");\n'
123 '</script>' % message
125 if message
== config
.FINISHED_MESSAGE
:
126 print '</body></html>'
128 except ValueError, e
:
129 log("failed to send message %r, got exception %r" % (message
, e
))
130 return progress_notifier
133 def get_page_settings(args
):
134 """Find the size and any optional layout settings.
136 args['booksize'] is either a keyword describing a size or
137 'custom'. If it is custom, the form is inspected for specific
138 dimensions -- otherwise these are ignored.
140 The margins, gutter, number of columns, and column
141 margins all set themselves automatically based on the page
142 dimensions, but they can be overridden. Any that are are
144 # get all the values including sizes first
145 # the sizes are found as 'page_width' and 'page_height',
146 # but the Book class expects them as a 'pointsize' tuple, so
147 # they are easily ignored.
149 for k
, extrema
in config
.PAGE_EXTREMA
.iteritems():
151 v
= float(args
.get(k
))
152 except (ValueError, TypeError):
153 #log("don't like %r as a float value for %s!" % (args.get(k), k))
155 min_val
, max_val
, multiplier
= extrema
156 if v
< min_val
or v
> max_val
:
157 log('rejecting %s: outside %s' % (v
,) + extrema
)
159 log('found %s=%s' % (k
, v
))
160 settings
[k
] = v
* multiplier
#convert to points in many cases
162 # now if args['size'] is not 'custom', the width and height found
164 size
= args
.get('booksize', config
.DEFAULT_SIZE
)
165 settings
.update(config
.PAGE_SIZE_DATA
[size
])
167 #if args['mode'] is 'newspaper', then the number of columns is
168 #automatically determined unless set -- otherwise default is 1.
169 if args
.get('mode') == 'newspaper' and settings
.get('columns') is None:
170 settings
['columns'] = 'auto'
172 if args
.get('grey_scale'):
173 settings
['grey_scale'] = True
176 #will raise KeyError if width, height aren't set
177 settings
['pointsize'] = (settings
['page_width'], settings
['page_height'])
178 del settings
['page_width']
179 del settings
['page_height']
181 settings
['engine'] = args
.get('engine', config
.DEFAULT_ENGINE
)
185 def mode_booklist(args
):
186 return optionise(get_book_list(args
.get('server', config
.DEFAULT_SERVER
)),
187 default
=args
.get('book'))
191 #XX sending as text/html, but it doesn't really matter
192 return get_default_css(args
.get('server', config
.DEFAULT_SERVER
), args
.get('pdftype', 'book'))
197 f
= open(config
.FORM_TEMPLATE
)
200 f
= open(config
.FONT_LIST_INCLUDE
)
201 font_list
= [x
.strip() for x
in f
if x
.strip()]
203 server
= args
.get('server', config
.DEFAULT_SERVER
)
204 book
= args
.get('book')
205 size
= args
.get('booksize', config
.DEFAULT_SIZE
)
206 engine
= args
.get('engine', config
.DEFAULT_ENGINE
)
208 'server_options': optionise(get_server_list(), default
=server
),
209 'book_options': optionise(get_book_list(server
), default
=book
),
210 'size_options': optionise(get_size_list(), default
=size
),
211 'engines': optionise(config
.ENGINES
.keys(), default
=engine
),
212 'pdf_types': optionise(sorted(k
for k
, v
in config
.CGI_MODES
.iteritems() if v
[0])),
213 'css': get_default_css(server
),
214 'font_links': listify(font_links()),
215 'font_list': listify(font_list
),
216 'default_license' : config
.DEFAULT_LICENSE
,
217 'licenses' : optionise(config
.LICENSES
, default
=config
.DEFAULT_LICENSE
),
223 for id, title
, type, source
, classes
, epilogue
in config
.FORM_INPUTS
:
224 val
= d
.get(source
, '')
225 e
= config
.FORM_ELEMENT_TYPES
[type] % locals()
226 form
.append('\n<div id="%(id)s_div" class="form-item %(classes)s">\n'
227 '<div class="input_title">%(title)s</div>\n'
228 '<div class="input_contents"> %(e)s %(epilogue)s\n</div>'
229 '</div>\n' % locals())
232 _valid_inputs
= set(ARG_VALIDATORS
)
233 _form_inputs
= set(x
[0] for x
in config
.FORM_INPUTS
if x
[2] != 'ul')
234 log("valid but not used inputs: %s" % (_valid_inputs
- _form_inputs
))
235 log("invalid form inputs: %s" % (_form_inputs
- _valid_inputs
))
237 return template
% {'form': ''.join(form
)}
240 def output_multi(book
, mimetype
, destination
):
241 if destination
== 'download':
242 f
= open(book
.publish_file
)
245 output_blob_and_exit(data
, mimetype
, book
.bookname
)
248 bookurl
= "http://%s/books/%s" % (HTTP_HOST
, book
.bookname
,)
250 bookurl
= "books/%s" % (book
.bookname
,)
252 if destination
== 'archive.org':
253 details_url
, s3url
= book
.publish_s3()
254 output_blob_and_exit("%s\n%s" % (bookurl
, details_url
), 'text/plain')
255 elif destination
== 'nowhere':
256 output_blob_and_exit(bookurl
, 'text/plain')
260 # so we're making a pdf.
261 mode
= args
.get('mode', 'book')
262 bookid
= args
.get('book')
263 server
= args
.get('server', config
.DEFAULT_SERVER
)
264 page_settings
= get_page_settings(args
)
265 bookname
= make_book_name(bookid
, server
)
266 destination
= args
.get('destination', config
.DEFAULT_CGI_DESTINATION
)
267 progress_bar
= make_progress_page(bookid
, bookname
, mode
, destination
)
269 with
Book(bookid
, server
, bookname
, page_settings
=page_settings
,
270 watchers
=[progress_bar
], isbn
=args
.get('isbn'),
271 license
=args
.get('license'), title
=args
.get('title'),
272 max_age
=float(args
.get('max-age', -1))) as book
:
276 if 'toc_header' in args
:
277 book
.toc_header
= args
['toc_header'].decode('utf-8')
279 book
.add_css(args
.get('css'), mode
)
280 book
.add_section_titles()
284 elif mode
in ('web', 'newspaper'):
285 book
.make_simple_pdf(mode
)
290 output_multi(book
, "application/pdf", destination
)
292 #These ones are similar enough to be handled by the one function
293 mode_newspaper
= mode_book
297 def mode_openoffice(args
):
298 """Make an openoffice document. A whole lot of the inputs have no
300 bookid
= args
.get('book')
301 server
= args
.get('server', config
.DEFAULT_SERVER
)
302 bookname
= make_book_name(bookid
, server
, '.odt')
303 destination
= args
.get('destination', config
.DEFAULT_CGI_DESTINATION
)
304 progress_bar
= make_progress_page(bookid
, bookname
, 'openoffice', destination
)
306 with
Book(bookid
, server
, bookname
,
307 watchers
=[progress_bar
], isbn
=args
.get('isbn'),
308 license
=args
.get('license'), title
=args
.get('title'),
309 max_age
=float(args
.get('max-age', -1))) as book
:
313 book
.add_css(args
.get('css'), 'openoffice')
314 book
.add_section_titles()
316 output_multi(book
, "application/vnd.oasis.opendocument.text", destination
)
320 log('making epub with\n%s' % pformat(args
))
321 #XXX need to catch and process lack of necessary arguments.
322 bookid
= args
.get('book')
323 server
= args
.get('server', config
.DEFAULT_SERVER
)
324 bookname
= make_book_name(bookid
, server
, '.epub')
325 destination
= args
.get('destination', config
.DEFAULT_CGI_DESTINATION
)
326 progress_bar
= make_progress_page(bookid
, bookname
, 'epub', destination
)
328 with
Book(bookid
, server
, bookname
=bookname
,
329 watchers
=[progress_bar
], title
=args
.get('title'),
330 max_age
=float(args
.get('max-age', -1))) as book
:
332 book
.make_epub(use_cache
=config
.USE_CACHED_IMAGES
)
333 output_multi(book
, "application/epub+zip", destination
)
336 def mode_bookizip(args
):
337 log('making bookizip with\n%s' % pformat(args
))
338 bookid
= args
.get('book')
339 server
= args
.get('server', config
.DEFAULT_SERVER
)
340 bookname
= make_book_name(bookid
, server
, '.zip')
342 destination
= args
.get('destination', config
.DEFAULT_CGI_DESTINATION
)
343 progress_bar
= make_progress_page(bookid
, bookname
, 'bookizip', destination
)
345 with
Book(bookid
, server
, bookname
=bookname
,
346 watchers
=[progress_bar
], title
=args
.get('title'),
347 max_age
=float(args
.get('max-age', -1))) as book
:
348 book
.publish_bookizip()
349 output_multi(book
, config
.BOOKIZIP_MIMETYPE
, destination
)
353 args
= parse_args(ARG_VALIDATORS
)
354 mode
= args
.get('mode')
355 if mode
is None and 'book' in args
:
359 CGI_CONTEXT
= 'SERVER_NAME' in os
.environ
or args
.get('cgi-context', 'no').lower() in '1true'
361 if not args
and not CGI_CONTEXT
:
365 output_function
= globals().get('mode_%s' % mode
, mode_form
)
366 output_function(args
)
369 if __name__
== '__main__':
370 if config
.CGITB_DOMAINS
and os
.environ
.get('REMOTE_ADDR') in config
.CGITB_DOMAINS
: