mirror of
https://github.com/Kozea/WeasyPrint.git
synced 2024-09-11 20:47:56 +03:00
Add API controlling JPEG quality
This commit is contained in:
parent
51971f3293
commit
eb6491f895
@ -370,7 +370,9 @@ def test_command_line_render(tmpdir):
|
||||
_run('not_optimized.html out22.pdf -O all -O none')
|
||||
_run('not_optimized.html out23.pdf -O pdf')
|
||||
_run('not_optimized.html out24.pdf -O none -O fonts -O pdf')
|
||||
_run('not_optimized.html out25.pdf -O all -j 10')
|
||||
assert (
|
||||
len(tmpdir.join('out25.pdf').read_binary()) <
|
||||
len(tmpdir.join('out16.pdf').read_binary()) <
|
||||
len(tmpdir.join('out15.pdf').read_binary()) <
|
||||
len(tmpdir.join('out20.pdf').read_binary()))
|
||||
|
@ -55,15 +55,16 @@ class FakeHTML(HTML):
|
||||
|
||||
def write_pdf(self, target=None, stylesheets=None, zoom=1,
|
||||
attachments=None, finisher=None, presentational_hints=False,
|
||||
optimize_size=('fonts',), font_config=None,
|
||||
counter_style=None, image_cache=None, identifier=None,
|
||||
variant=None, version=None, forms=False,
|
||||
optimize_size=('fonts',), jpeg_quality=None,
|
||||
font_config=None, counter_style=None, image_cache=None,
|
||||
identifier=None, variant=None, version=None, forms=False,
|
||||
custom_metadata=False):
|
||||
# Override function to set PDF size optimization to False by default
|
||||
return super().write_pdf(
|
||||
target, stylesheets, zoom, attachments, finisher,
|
||||
presentational_hints, optimize_size, font_config, counter_style,
|
||||
image_cache, identifier, variant, version, forms, custom_metadata)
|
||||
presentational_hints, optimize_size, jpeg_quality, font_config,
|
||||
counter_style, image_cache, identifier, variant, version, forms,
|
||||
custom_metadata)
|
||||
|
||||
|
||||
def resource_filename(basename):
|
||||
@ -194,7 +195,7 @@ def _parse_base(html_content, base_url=BASE_URL):
|
||||
style_for = get_all_computed_styles(document, counter_style=counter_style)
|
||||
get_image_from_uri = functools.partial(
|
||||
images.get_image_from_uri, cache={}, url_fetcher=document.url_fetcher,
|
||||
optimize_size=())
|
||||
optimize_size=(), jpeg_quality=None)
|
||||
target_collector = TargetCollector()
|
||||
footnotes = []
|
||||
return (
|
||||
|
@ -118,8 +118,9 @@ class HTML:
|
||||
return [HTML5_PH_STYLESHEET]
|
||||
|
||||
def render(self, stylesheets=None, presentational_hints=False,
|
||||
optimize_size=('fonts', 'pdf'), font_config=None,
|
||||
counter_style=None, image_cache=None, forms=False):
|
||||
optimize_size=('fonts', 'pdf'), jpeg_quality=None,
|
||||
font_config=None, counter_style=None, image_cache=None,
|
||||
forms=False):
|
||||
"""Lay out and paginate the document, but do not (yet) export it.
|
||||
|
||||
This returns a :class:`document.Document` object which provides
|
||||
@ -135,6 +136,7 @@ class HTML:
|
||||
:param tuple optimize_size:
|
||||
Optimize size of generated PDF. Can contain "images", "fonts" and
|
||||
"pdf".
|
||||
:param int jpeg_quality: JPEG quality between 0 (worst) to 95 (best).
|
||||
:type font_config: :class:`text.fonts.FontConfiguration`
|
||||
:param font_config: A font configuration handling ``@font-face`` rules.
|
||||
:type counter_style: :class:`css.counters.CounterStyle`
|
||||
@ -150,13 +152,13 @@ class HTML:
|
||||
"""
|
||||
return Document._render(
|
||||
self, stylesheets, presentational_hints, optimize_size,
|
||||
font_config, counter_style, image_cache, forms)
|
||||
jpeg_quality, font_config, counter_style, image_cache, forms)
|
||||
|
||||
def write_pdf(self, target=None, stylesheets=None, zoom=1,
|
||||
attachments=None, finisher=None, presentational_hints=False,
|
||||
optimize_size=('fonts', 'pdf'), font_config=None,
|
||||
counter_style=None, image_cache=None, identifier=None,
|
||||
variant=None, version=None, forms=False,
|
||||
optimize_size=('fonts', 'pdf'), jpeg_quality=None,
|
||||
font_config=None, counter_style=None, image_cache=None,
|
||||
identifier=None, variant=None, version=None, forms=False,
|
||||
custom_metadata=False):
|
||||
"""Render the document to a PDF file.
|
||||
|
||||
@ -188,6 +190,7 @@ class HTML:
|
||||
:param tuple optimize_size:
|
||||
Optimize size of generated PDF. Can contain "images", "fonts" and
|
||||
"pdf".
|
||||
:param int jpeg_quality: JPEG quality between 0 (worst) to 95 (best).
|
||||
:type font_config: :class:`text.fonts.FontConfiguration`
|
||||
:param font_config: A font configuration handling ``@font-face`` rules.
|
||||
:type counter_style: :class:`css.counters.CounterStyle`
|
||||
@ -211,8 +214,8 @@ class HTML:
|
||||
"""
|
||||
return (
|
||||
self.render(
|
||||
stylesheets, presentational_hints, optimize_size, font_config,
|
||||
counter_style, image_cache, forms)
|
||||
stylesheets, presentational_hints, optimize_size, jpeg_quality,
|
||||
font_config, counter_style, image_cache, forms)
|
||||
.write_pdf(
|
||||
target, zoom, attachments, finisher, identifier, variant,
|
||||
version, custom_metadata))
|
||||
|
@ -100,6 +100,10 @@ def main(argv=None, stdout=None, stdin=None):
|
||||
Store cache on disk instead of memory. The ``folder`` is created if
|
||||
needed and cleaned after the PDF is generated.
|
||||
|
||||
.. option:: -j <quality>, --jpeg-quality <quality>
|
||||
|
||||
JPEG quality between 0 (worst) to 95 (best).
|
||||
|
||||
.. option:: -v, --verbose
|
||||
|
||||
Show warnings and information messages.
|
||||
@ -167,6 +171,9 @@ def main(argv=None, stdout=None, stdin=None):
|
||||
'-c', '--cache-folder',
|
||||
help='Store cache on disk instead of memory. The ``folder`` is '
|
||||
'created if needed and cleaned after the PDF is generated.')
|
||||
parser.add_argument(
|
||||
'-j', '--jpeg-quality', type=int,
|
||||
help='JPEG quality between 0 (worst) to 95 (best)')
|
||||
parser.add_argument(
|
||||
'-v', '--verbose', action='store_true',
|
||||
help='show warnings and information messages')
|
||||
@ -208,6 +215,7 @@ def main(argv=None, stdout=None, stdin=None):
|
||||
'stylesheets': args.stylesheet,
|
||||
'presentational_hints': args.presentational_hints,
|
||||
'optimize_size': tuple(optimize_size),
|
||||
'jpeg_quality': args.jpeg_quality,
|
||||
'attachments': args.attachment,
|
||||
'identifier': args.pdf_identifier,
|
||||
'variant': args.pdf_variant,
|
||||
|
@ -219,8 +219,8 @@ class Document:
|
||||
|
||||
@classmethod
|
||||
def _build_layout_context(cls, html, stylesheets, presentational_hints,
|
||||
optimize_size, font_config, counter_style,
|
||||
image_cache, forms):
|
||||
optimize_size, jpeg_quality, font_config,
|
||||
counter_style, image_cache, forms):
|
||||
if font_config is None:
|
||||
font_config = FontConfiguration()
|
||||
if counter_style is None:
|
||||
@ -243,7 +243,8 @@ class Document:
|
||||
counter_style, page_rules, target_collector, forms)
|
||||
get_image_from_uri = functools.partial(
|
||||
original_get_image_from_uri, cache=image_cache,
|
||||
url_fetcher=html.url_fetcher, optimize_size=optimize_size)
|
||||
url_fetcher=html.url_fetcher, optimize_size=optimize_size,
|
||||
jpeg_quality=jpeg_quality)
|
||||
PROGRESS_LOGGER.info('Step 4 - Creating formatting structure')
|
||||
context = LayoutContext(
|
||||
style_for, get_image_from_uri, font_config, counter_style,
|
||||
@ -252,7 +253,7 @@ class Document:
|
||||
|
||||
@classmethod
|
||||
def _render(cls, html, stylesheets, presentational_hints, optimize_size,
|
||||
font_config, counter_style, image_cache, forms):
|
||||
jpeg_quality, font_config, counter_style, image_cache, forms):
|
||||
if font_config is None:
|
||||
font_config = FontConfiguration()
|
||||
|
||||
@ -261,7 +262,7 @@ class Document:
|
||||
|
||||
context = cls._build_layout_context(
|
||||
html, stylesheets, presentational_hints, optimize_size,
|
||||
font_config, counter_style, image_cache, forms)
|
||||
jpeg_quality, font_config, counter_style, image_cache, forms)
|
||||
|
||||
root_box = build_formatting_structure(
|
||||
html.etree_element, context.style_for, context.get_image_from_uri,
|
||||
|
@ -1202,8 +1202,7 @@ def draw_first_line(stream, textbox, text_overflow, block_ellipsis, x, y,
|
||||
png_data = ffi.unpack(hb_data, int(stream.length[0]))
|
||||
pillow_image = Image.open(BytesIO(png_data))
|
||||
image_id = f'{font.hash}{glyph}'
|
||||
image = RasterImage(
|
||||
pillow_image, image_id, optimize_size=(), cache={})
|
||||
image = RasterImage(pillow_image, image_id)
|
||||
d = font.widths[glyph] / 1000
|
||||
a = pillow_image.width / pillow_image.height * d
|
||||
pango.pango_font_get_glyph_extents(
|
||||
|
@ -36,9 +36,17 @@ class ImageLoadingError(ValueError):
|
||||
|
||||
|
||||
class RasterImage:
|
||||
def __init__(self, pillow_image, image_id, optimize_size, cache):
|
||||
def __init__(self, pillow_image, image_id, cache=None, optimize_size=(),
|
||||
jpeg_quality=None):
|
||||
self.id = image_id
|
||||
self._cache = cache
|
||||
self._cache = {} if cache is None else cache
|
||||
self._optimize_size = optimize_size
|
||||
self._jpeg_quality = jpeg_quality
|
||||
self._intrinsic_width = pillow_image.width
|
||||
self._intrinsic_height = pillow_image.height
|
||||
self._intrinsic_ratio = (
|
||||
self._intrinsic_width / self._intrinsic_height
|
||||
if self._intrinsic_height != 0 else inf)
|
||||
|
||||
if 'transparency' in pillow_image.info:
|
||||
pillow_image = pillow_image.convert('RGBA')
|
||||
@ -71,7 +79,10 @@ class RasterImage:
|
||||
if pillow_image.format in ('JPEG', 'MPO'):
|
||||
self.extra['Filter'] = '/DCTDecode'
|
||||
image_file = io.BytesIO()
|
||||
pillow_image.save(image_file, format='JPEG', optimize=optimize)
|
||||
options = {'format': 'JPEG', 'optimize': optimize}
|
||||
if jpeg_quality is not None:
|
||||
options['quality'] = jpeg_quality
|
||||
pillow_image.save(image_file, **options)
|
||||
self.stream = self.get_stream(image_file.getvalue())
|
||||
else:
|
||||
self.extra['Filter'] = '/FlateDecode'
|
||||
@ -116,7 +127,6 @@ class RasterImage:
|
||||
def draw(self, stream, concrete_width, concrete_height, image_rendering):
|
||||
if self.width <= 0 or self.height <= 0:
|
||||
return
|
||||
|
||||
image_name = stream.add_image(self, image_rendering)
|
||||
stream.transform(
|
||||
concrete_width, 0, 0, -concrete_height, 0, concrete_height)
|
||||
@ -138,12 +148,12 @@ class RasterImage:
|
||||
# Each chunk begins with its data length (four bytes, may be zero),
|
||||
# then its type (four ASCII characters), then the data, then four
|
||||
# bytes of a CRC.
|
||||
chunk_len, = struct.unpack('!I', raw_chunk_length)
|
||||
chunk_length, = struct.unpack('!I', raw_chunk_length)
|
||||
chunk_type = image_file.read(4)
|
||||
if chunk_type == b'IDAT':
|
||||
png_data.append(image_file.read(chunk_len))
|
||||
png_data.append(image_file.read(chunk_length))
|
||||
else:
|
||||
image_file.seek(chunk_len, io.SEEK_CUR)
|
||||
image_file.seek(chunk_length, io.SEEK_CUR)
|
||||
# We aren't checking the CRC, we assume this is a valid PNG.
|
||||
image_file.seek(4, io.SEEK_CUR)
|
||||
raw_chunk_length = image_file.read(4)
|
||||
@ -198,7 +208,7 @@ class SVGImage:
|
||||
self._url_fetcher, self._context)
|
||||
|
||||
|
||||
def get_image_from_uri(cache, url_fetcher, optimize_size, url,
|
||||
def get_image_from_uri(cache, url_fetcher, optimize_size, jpeg_quality, url,
|
||||
forced_mime_type=None, context=None,
|
||||
orientation='from-image'):
|
||||
"""Get an Image instance from an image URI."""
|
||||
@ -242,7 +252,7 @@ def get_image_from_uri(cache, url_fetcher, optimize_size, url,
|
||||
image_id = md5(url.encode()).hexdigest()
|
||||
pillow_image = rotate_pillow_image(pillow_image, orientation)
|
||||
image = RasterImage(
|
||||
pillow_image, image_id, optimize_size, cache)
|
||||
pillow_image, image_id, cache, optimize_size, jpeg_quality)
|
||||
|
||||
except (URLFetchingError, ImageLoadingError) as exception:
|
||||
LOGGER.error('Failed to load image at %r: %s', url, exception)
|
||||
|
@ -377,7 +377,7 @@ class Stream(pydyf.Stream):
|
||||
extra['SMask'].compress)
|
||||
extra['SMask'].extra['Interpolate'] = interpolate
|
||||
|
||||
xobject = pydyf.Stream(image.stream, extra=extra)
|
||||
xobject = pydyf.Stream(image.stream, extra)
|
||||
self._images[image_name] = xobject
|
||||
return image_name
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user