-
Notifications
You must be signed in to change notification settings - Fork 8
/
html2pdfd.py
executable file
·126 lines (112 loc) · 4.14 KB
/
html2pdfd.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
#!/usr/bin/env python3
import argparse
import io
import json
import logging
from aiohttp.web import Application, run_app
from aiohttp_wsgi import WSGIHandler
from wand.color import Color
from wand.image import Image
from weasyprint import HTML
from werkzeug.wrappers import Request, Response
from werkzeug.serving import run_simple
__all__ = 'app',
SUPPORTED_TYPES = {
'application/pdf': lambda html, buffer: html.write_pdf(buffer),
'image/png': lambda html, buffer: html.write_png(buffer),
'image/jpeg': lambda html, buffer: render_to_jpeg(html, buffer),
}
MAX_HTML_SIZE = 1024 * 1024 * 50 # 50MiB
def render_to_jpeg(html: HTML, buffer: io.BytesIO):
png_buffer = io.BytesIO()
html.write_png(png_buffer)
png_buffer.seek(0)
with Image(file=png_buffer) as image:
image.background_color = Color('#fff')
image.alpha_channel = 'remove'
image.format = 'jpeg'
image.save(file=buffer)
@Request.application
def app(request: Request) -> Response:
if request.path != '/':
return Response(
json.dumps({
'error': 'not-found',
'message': "page not found; there's only one path: /"
}),
status=404
)
elif request.method.upper() != 'POST':
return Response(
json.dumps({
'error': 'method-not-allowed',
'message': 'only POST method is allowed'
}),
status=405
)
elif request.mimetype not in {'text/html', 'application/xhtml+xml'}:
return Response(
json.dumps({
'error': 'bad-request',
'message': 'content has to be HTML'
}),
status=400
)
supported_types = sorted(SUPPORTED_TYPES)
matched = request.accept_mimetypes.best_match(supported_types,
default='application/pdf')
if not matched:
return Response(
json.dumps({
'error': 'not-acceptable',
'message': 'unsupported type; the list of supported '
'types: ' + ', '.join(SUPPORTED_TYPES)
}),
status=406
)
html = HTML(string=request.get_data(as_text=True))
pdf_buffer = io.BytesIO()
SUPPORTED_TYPES[matched](html, pdf_buffer)
pdf_buffer.seek(0)
return Response(pdf_buffer, mimetype=matched)
def main():
parser = argparse.ArgumentParser(
description='HTTP server that renders HTML to PDF'
)
parser.add_argument('--host', '-H',
default='0.0.0.0', help='host to listen [%(default)s]')
parser.add_argument('--port', '-p',
type=int, default=8080,
help='port to listen [%(default)s]')
parser.add_argument('--pong-path',
help='pong path to respond to to ping (e.g. /pong/)')
parser.add_argument('--debug', '-d',
action='store_true', help='debug mode')
args = parser.parse_args()
pong_path = args.pong_path
if pong_path is None:
wsgi_app = app
else:
if not pong_path.startswith('/'):
parser.error('--pong-path value must start with a slash (/)')
return
@Request.application
def wsgi_app(request: Request):
if request.path == pong_path:
return Response('true', mimetype='application/json')
return app
if args.debug:
run_simple(args.host, args.port, wsgi_app,
use_debugger=True, use_reloader=True)
else:
logging.basicConfig(level=logging.INFO, format='%(message)s')
for logger_name in 'html2pdfd', 'aiohttp', 'aiohttp_wsgi':
logging.getLogger(logger_name).setLevel(logging.INFO)
wsgi_handler = WSGIHandler(wsgi_app)
aio_app = Application()
aio_app.router.add_route('*', '/{path_info:.*}', wsgi_handler)
logging.getLogger('html2pdfd').info('Serving on http://%s:%d',
args.host, args.port)
run_app(aio_app, host=args.host, port=args.port)
if __name__ == '__main__':
main()