1 # GNU MediaGoblin -- federated, autonomous media hosting
2 # Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License as published by
6 # the Free Software Foundation, either version 3 of the License, or
7 # (at your option) any later version.
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU Affero General Public License for more details.
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 import dateutil
.parser
20 from subprocess
import PIPE
, Popen
22 from mediagoblin
import mg_globals
as mgg
23 from mediagoblin
.processing
import (
24 FilenameBuilder
, BadMediaFail
,
25 MediaProcessor
, ProcessingManager
,
26 request_from_args
, get_process_filename
,
27 store_public
, copy_original
)
28 from mediagoblin
.tools
.translate
import fake_ugettext_passthrough
as _
30 _log
= logging
.getLogger(__name__
)
32 MEDIA_TYPE
= 'mediagoblin.media_types.pdf'
34 # TODO - cache (memoize) util
36 # This is a list created via uniconv --show and hand removing some types that
37 # we already support via other media types better.
39 'bib', # - BibTeX [.bib]
40 #bmp - Windows Bitmap [.bmp]
41 'csv', # - Text CSV [.csv]
42 'dbf', # - dBASE [.dbf]
43 'dif', # - Data Interchange Format [.dif]
44 'doc6', # - Microsoft Word 6.0 [.doc]
45 'doc95', # - Microsoft Word 95 [.doc]
46 'docbook', # - DocBook [.xml]
47 'doc', # - Microsoft Word 97/2000/XP [.doc]
48 'docx7', # - Microsoft Office Open XML [.docx]
49 'docx', # - Microsoft Office Open XML [.docx]
50 #emf - Enhanced Metafile [.emf]
51 'eps', # - Encapsulated PostScript [.eps]
52 'fodp', # - OpenDocument Presentation (Flat XML) [.fodp]
53 'fods', # - OpenDocument Spreadsheet (Flat XML) [.fods]
54 'fodt', # - OpenDocument Text (Flat XML) [.fodt]
55 #gif - Graphics Interchange Format [.gif]
56 'html', # - HTML Document (OpenOffice.org Writer) [.html]
57 #jpg - Joint Photographic Experts Group [.jpg]
58 'latex', # - LaTeX 2e [.ltx]
59 'mediawiki', # - MediaWiki [.txt]
60 'met', # - OS/2 Metafile [.met]
61 'odd', # - OpenDocument Drawing [.odd]
62 'odg', # - ODF Drawing (Impress) [.odg]
63 'odp', # - ODF Presentation [.odp]
64 'ods', # - ODF Spreadsheet [.ods]
65 'odt', # - ODF Text Document [.odt]
66 'ooxml', # - Microsoft Office Open XML [.xml]
67 'otg', # - OpenDocument Drawing Template [.otg]
68 'otp', # - ODF Presentation Template [.otp]
69 'ots', # - ODF Spreadsheet Template [.ots]
70 'ott', # - Open Document Text [.ott]
71 #pbm - Portable Bitmap [.pbm]
72 #pct - Mac Pict [.pct]
73 'pdb', # - AportisDoc (Palm) [.pdb]
74 #pdf - Portable Document Format [.pdf]
75 #pgm - Portable Graymap [.pgm]
76 #png - Portable Network Graphic [.png]
77 'pot', # - Microsoft PowerPoint 97/2000/XP Template [.pot]
78 'potm', # - Microsoft PowerPoint 2007/2010 XML Template [.potm]
79 #ppm - Portable Pixelmap [.ppm]
80 'pps', # - Microsoft PowerPoint 97/2000/XP (Autoplay) [.pps]
81 'ppt', # - Microsoft PowerPoint 97/2000/XP [.ppt]
82 'pptx', # - Microsoft PowerPoint 2007/2010 XML [.pptx]
83 'psw', # - Pocket Word [.psw]
84 'pwp', # - PlaceWare [.pwp]
85 'pxl', # - Pocket Excel [.pxl]
86 #ras - Sun Raster Image [.ras]
87 'rtf', # - Rich Text Format [.rtf]
88 'sda', # - StarDraw 5.0 (OpenOffice.org Impress) [.sda]
89 'sdc3', # - StarCalc 3.0 [.sdc]
90 'sdc4', # - StarCalc 4.0 [.sdc]
91 'sdc', # - StarCalc 5.0 [.sdc]
92 'sdd3', # - StarDraw 3.0 (OpenOffice.org Impress) [.sdd]
93 'sdd4', # - StarImpress 4.0 [.sdd]
94 'sdd', # - StarImpress 5.0 [.sdd]
95 'sdw3', # - StarWriter 3.0 [.sdw]
96 'sdw4', # - StarWriter 4.0 [.sdw]
97 'sdw', # - StarWriter 5.0 [.sdw]
98 'slk', # - SYLK [.slk]
99 'stc', # - OpenOffice.org 1.0 Spreadsheet Template [.stc]
100 'std', # - OpenOffice.org 1.0 Drawing Template [.std]
101 'sti', # - OpenOffice.org 1.0 Presentation Template [.sti]
102 'stw', # - Open Office.org 1.0 Text Document Template [.stw]
103 #svg - Scalable Vector Graphics [.svg]
104 'svm', # - StarView Metafile [.svm]
105 'swf', # - Macromedia Flash (SWF) [.swf]
106 'sxc', # - OpenOffice.org 1.0 Spreadsheet [.sxc]
107 'sxd3', # - StarDraw 3.0 [.sxd]
108 'sxd5', # - StarDraw 5.0 [.sxd]
109 'sxd', # - OpenOffice.org 1.0 Drawing (OpenOffice.org Impress) [.sxd]
110 'sxi', # - OpenOffice.org 1.0 Presentation [.sxi]
111 'sxw', # - Open Office.org 1.0 Text Document [.sxw]
112 #text - Text Encoded [.txt]
113 #tiff - Tagged Image File Format [.tiff]
115 'uop', # - Unified Office Format presentation [.uop]
116 'uos', # - Unified Office Format spreadsheet [.uos]
117 'uot', # - Unified Office Format text [.uot]
118 'vor3', # - StarDraw 3.0 Template (OpenOffice.org Impress) [.vor]
119 'vor4', # - StarWriter 4.0 Template [.vor]
120 'vor5', # - StarDraw 5.0 Template (OpenOffice.org Impress) [.vor]
121 'vor', # - StarCalc 5.0 Template [.vor]
122 #wmf - Windows Metafile [.wmf]
123 'xhtml', # - XHTML Document [.html]
124 'xls5', # - Microsoft Excel 5.0 [.xls]
125 'xls95', # - Microsoft Excel 95 [.xls]
126 'xls', # - Microsoft Excel 97/2000/XP [.xls]
127 'xlt5', # - Microsoft Excel 5.0 Template [.xlt]
128 'xlt95', # - Microsoft Excel 95 Template [.xlt]
129 'xlt', # - Microsoft Excel 97/2000/XP Template [.xlt]
130 #xpm - X PixMap [.xpm]
133 def is_unoconv_working():
134 # TODO: must have libreoffice-headless installed too, need to check for it
135 unoconv
= where('unoconv')
139 proc
= Popen([unoconv
, '--show'], stderr
=PIPE
)
140 output
= proc
.stderr
.read()
142 _log
.warn(_('unoconv failing to run, check log file'))
144 if b
'ERROR' in output
:
148 def supported_extensions(cache
=[None]):
151 if is_unoconv_working():
152 cache
.extend(unoconv_supported
)
156 for p
in os
.environ
['PATH'].split(os
.pathsep
):
157 fullpath
= os
.path
.join(p
, name
)
158 if os
.path
.exists(fullpath
):
162 def check_prerequisites():
163 if not where('pdfinfo'):
164 _log
.warn('missing pdfinfo')
166 if not where('pdftocairo'):
167 _log
.warn('missing pdfcairo')
171 def sniff_handler(media_file
, filename
):
172 _log
.info('Sniffing {0}'.format(MEDIA_TYPE
))
173 if not check_prerequisites():
176 name
, ext
= os
.path
.splitext(filename
)
177 clean_ext
= ext
[1:].lower()
179 if clean_ext
in supported_extensions():
182 def create_pdf_thumb(original
, thumb_filename
, width
, height
):
183 # Note: pdftocairo adds '.png', remove it
184 thumb_filename
= thumb_filename
[:-4]
185 executable
= where('pdftocairo')
186 args
= [executable
, '-scale-to', str(min(width
, height
)),
187 '-singlefile', '-png', original
, thumb_filename
]
188 _log
.debug('calling {0}'.format(repr(' '.join(args
))))
189 Popen(executable
=executable
, args
=args
).wait()
191 def pdf_info(original
):
193 Extract dictionary of pdf information. This could use a library instead
196 Note: I'm assuming pdfinfo output is sanitized (integers where integers are
197 expected, etc.) - if this is wrong then an exception will be raised and caught
198 leading to the dreaded error page. It seems a safe assumption.
201 pdfinfo
= where('pdfinfo')
203 proc
= Popen(executable
=pdfinfo
,
204 args
=[pdfinfo
, original
], stdout
=PIPE
)
205 lines
= proc
.stdout
.readlines()
207 _log
.debug('pdfinfo could not read the pdf file.')
210 lines
= [l
.decode('utf-8', 'replace') for l
in lines
]
211 info_dict
= dict([[part
.strip() for part
in l
.strip().split(':', 1)]
212 for l
in lines
if ':' in l
])
214 if 'Page size' not in info_dict
.keys():
215 # TODO - message is for the user, not debug, but BadMediaFail not taking an argument, fix that.
216 _log
.debug('Missing "Page size" key in returned pdf - conversion failed?')
219 for date_key
in [('pdf_mod_date', 'ModDate'),
220 ('pdf_creation_date', 'CreationDate')]:
221 if date_key
in info_dict
:
222 ret_dict
[date_key
] = dateutil
.parser
.parse(info_dict
[date_key
])
223 for db_key
, int_key
in [('pdf_pages', 'Pages')]:
224 if int_key
in info_dict
:
225 ret_dict
[db_key
] = int(info_dict
[int_key
])
227 # parse 'PageSize' field: 595 x 842 pts (A4)
228 page_size_parts
= info_dict
['Page size'].split()
229 ret_dict
['pdf_page_size_width'] = float(page_size_parts
[0])
230 ret_dict
['pdf_page_size_height'] = float(page_size_parts
[2])
232 for db_key
, str_key
in [('pdf_keywords', 'Keywords'),
233 ('pdf_creator', 'Creator'), ('pdf_producer', 'Producer'),
234 ('pdf_author', 'Author'), ('pdf_title', 'Title')]:
235 ret_dict
[db_key
] = info_dict
.get(str_key
, None)
236 ret_dict
['pdf_version_major'], ret_dict
['pdf_version_minor'] = \
237 map(int, info_dict
['PDF version'].split('.'))
242 class CommonPdfProcessor(MediaProcessor
):
244 Provides a base for various pdf processing steps
246 acceptable_files
= ['original', 'pdf']
248 def common_setup(self
):
250 Set up common pdf processing steps
252 # Pull down and set up the processing file
253 self
.process_filename
= get_process_filename(
254 self
.entry
, self
.workbench
, self
.acceptable_files
)
255 self
.name_builder
= FilenameBuilder(self
.process_filename
)
257 self
._set
_pdf
_filename
()
259 def _set_pdf_filename(self
):
260 if self
.name_builder
.ext
== '.pdf':
261 self
.pdf_filename
= self
.process_filename
262 elif self
.entry
.media_files
.get('pdf'):
263 self
.pdf_filename
= self
.workbench
.localized_file(
264 mgg
.public_store
, self
.entry
.media_files
['pdf'])
266 self
.pdf_filename
= self
._generate
_pdf
()
268 def _skip_processing(self
, keyname
, **kwargs
):
269 file_metadata
= self
.entry
.get_file_metadata(keyname
)
272 if not file_metadata
:
275 if keyname
== 'thumb':
276 if kwargs
.get('thumb_size') != file_metadata
.get('thumb_size'):
278 elif keyname
== 'medium':
279 if kwargs
.get('size') != file_metadata
.get('size'):
284 def copy_original(self
):
286 self
.entry
, self
.process_filename
,
287 self
.name_builder
.fill('{basename}{ext}'))
289 def generate_thumb(self
, thumb_size
=None):
291 thumb_size
= (mgg
.global_config
['media:thumb']['max_width'],
292 mgg
.global_config
['media:thumb']['max_height'])
294 if self
._skip
_processing
('thumb', thumb_size
=thumb_size
):
297 # Note: pdftocairo adds '.png', so don't include an ext
298 thumb_filename
= os
.path
.join(self
.workbench
.dir,
299 self
.name_builder
.fill(
300 '{basename}.thumbnail'))
302 executable
= where('pdftocairo')
303 args
= [executable
, '-scale-to', str(min(thumb_size
)),
304 '-singlefile', '-png', self
.pdf_filename
, thumb_filename
]
306 _log
.debug('calling {0}'.format(repr(' '.join(args
))))
307 Popen(executable
=executable
, args
=args
).wait()
309 # since pdftocairo added '.png', we need to include it with the
311 store_public(self
.entry
, 'thumb', thumb_filename
+ '.png',
312 self
.name_builder
.fill('{basename}.thumbnail.png'))
314 self
.entry
.set_file_metadata('thumb', thumb_size
=thumb_size
)
316 def _generate_pdf(self
):
318 Store the pdf. If the file is not a pdf, make it a pdf
320 tmp_pdf
= os
.path
.splitext(self
.process_filename
)[0] + '.pdf'
322 unoconv
= where('unoconv')
323 args
= [unoconv
, '-v', '-f', 'pdf', self
.process_filename
]
324 _log
.debug('calling %s' % repr(args
))
325 Popen(executable
=unoconv
,
328 if not os
.path
.exists(tmp_pdf
):
329 _log
.debug('unoconv failed to convert file to pdf')
332 store_public(self
.entry
, 'pdf', tmp_pdf
,
333 self
.name_builder
.fill('{basename}.pdf'))
335 return self
.workbench
.localized_file(
336 mgg
.public_store
, self
.entry
.media_files
['pdf'])
338 def extract_pdf_info(self
):
339 pdf_info_dict
= pdf_info(self
.pdf_filename
)
340 self
.entry
.media_data_init(**pdf_info_dict
)
342 def generate_medium(self
, size
=None):
344 size
= (mgg
.global_config
['media:medium']['max_width'],
345 mgg
.global_config
['media:medium']['max_height'])
347 if self
._skip
_processing
('medium', size
=size
):
350 # Note: pdftocairo adds '.png', so don't include an ext
351 filename
= os
.path
.join(self
.workbench
.dir,
352 self
.name_builder
.fill('{basename}.medium'))
354 executable
= where('pdftocairo')
355 args
= [executable
, '-scale-to', str(min(size
)),
356 '-singlefile', '-png', self
.pdf_filename
, filename
]
358 _log
.debug('calling {0}'.format(repr(' '.join(args
))))
359 Popen(executable
=executable
, args
=args
).wait()
361 # since pdftocairo added '.png', we need to include it with the
363 store_public(self
.entry
, 'medium', filename
+ '.png',
364 self
.name_builder
.fill('{basename}.medium.png'))
366 self
.entry
.set_file_metadata('medium', size
=size
)
369 class InitialProcessor(CommonPdfProcessor
):
371 Initial processing step for new pdfs
374 description
= "Initial processing"
377 def media_is_eligible(cls
, entry
=None, state
=None):
379 Determine if this media type is eligible for processing
384 "unprocessed", "failed")
387 def generate_parser(cls
):
388 parser
= argparse
.ArgumentParser(
389 description
=cls
.description
,
395 metavar
=('max_width', 'max_height'),
401 metavar
=('max_width', 'max_height'),
407 def args_to_request(cls
, args
):
408 return request_from_args(
409 args
, ['size', 'thumb_size'])
411 def process(self
, size
=None, thumb_size
=None):
413 self
.extract_pdf_info()
415 self
.generate_medium(size
=size
)
416 self
.generate_thumb(thumb_size
=thumb_size
)
417 self
.delete_queue_file()
420 class Resizer(CommonPdfProcessor
):
422 Resizing process steps for processed pdfs
425 description
= 'Resize thumbnail and medium'
429 def media_is_eligible(cls
, entry
=None, state
=None):
431 Determine if this media type is eligible for processing
435 return state
in 'processed'
438 def generate_parser(cls
):
439 parser
= argparse
.ArgumentParser(
440 description
=cls
.description
,
446 metavar
=('max_width', 'max_height'),
451 choices
=['medium', 'thumb'])
456 def args_to_request(cls
, args
):
457 return request_from_args(
458 args
, ['size', 'file'])
460 def process(self
, file, size
=None):
463 self
.generate_medium(size
=size
)
464 elif file == 'thumb':
465 self
.generate_thumb(thumb_size
=size
)
468 class PdfProcessingManager(ProcessingManager
):
470 super(PdfProcessingManager
, self
).__init
__()
471 self
.add_processor(InitialProcessor
)
472 self
.add_processor(Resizer
)