images.py 6.56 KB
Newer Older
1
import imghdr
jvoisin's avatar
jvoisin committed
2
import os
jvoisin's avatar
jvoisin committed
3 4
import re
from typing import Set, Dict, Union, Any
jvoisin's avatar
jvoisin committed
5

6 7
import cairo

jvoisin's avatar
jvoisin committed
8 9
import gi
gi.require_version('GdkPixbuf', '2.0')
jvoisin's avatar
jvoisin committed
10 11
gi.require_version('Rsvg', '2.0')
from gi.repository import GdkPixbuf, GLib, Rsvg
jvoisin's avatar
jvoisin committed
12

jvoisin's avatar
jvoisin committed
13
from . import exiftool, abstract
jvoisin's avatar
jvoisin committed
14

15 16
# Make pyflakes happy
assert Set
jvoisin's avatar
jvoisin committed
17
assert Any
18

jvoisin's avatar
jvoisin committed
19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
class SVGParser(exiftool.ExiftoolParser):
    mimetypes = {'image/svg+xml', }
    meta_allowlist = {'Directory', 'ExifToolVersion', 'FileAccessDate',
                      'FileInodeChangeDate', 'FileModifyDate', 'FileName',
                      'FilePermissions', 'FileSize', 'FileType',
                      'FileTypeExtension', 'ImageHeight', 'ImageWidth',
                      'MIMEType', 'SVGVersion', 'SourceFile', 'ViewBox'
                      }

    def remove_all(self) -> bool:
        svg = Rsvg.Handle.new_from_file(self.filename)
        dimensions = svg.get_dimensions()
        surface = cairo.SVGSurface(self.output_filename,
                                   dimensions.height,
                                   dimensions.width)
        context = cairo.Context(surface)
        svg.render_cairo(context)
        surface.finish()
        return True

    def get_meta(self) -> Dict[str, Union[str, dict]]:
        meta = super().get_meta()

jvoisin's avatar
jvoisin committed
42
        # The namespace is mandatory, but only the …/2000/svg is valid.
jvoisin's avatar
jvoisin committed
43 44 45 46 47
        ns = 'http://www.w3.org/2000/svg'
        if meta.get('Xmlns', ns) == ns:
            meta.pop('Xmlns')
        return meta

48
class PNGParser(exiftool.ExiftoolParser):
49
    mimetypes = {'image/png', }
50
    meta_allowlist = {'SourceFile', 'ExifToolVersion', 'FileName',
jvoisin's avatar
jvoisin committed
51 52 53 54 55 56
                      'Directory', 'FileSize', 'FileModifyDate',
                      'FileAccessDate', 'FileInodeChangeDate',
                      'FilePermissions', 'FileType', 'FileTypeExtension',
                      'MIMEType', 'ImageWidth', 'BitDepth', 'ColorType',
                      'Compression', 'Filter', 'Interlace', 'BackgroundColor',
                      'ImageSize', 'Megapixels', 'ImageHeight'}
57

58 59
    def __init__(self, filename):
        super().__init__(filename)
60 61 62 63

        if imghdr.what(filename) != 'png':
            raise ValueError

64 65
        try:  # better fail here than later
            cairo.ImageSurface.create_from_png(self.filename)
66 67
        except Exception:  # pragma: no cover
            # Cairo is returning some weird exceptions :/
68 69
            raise ValueError

jvoisin's avatar
jvoisin committed
70
    def remove_all(self) -> bool:
71 72
        if self.lightweight_cleaning:
            return self._lightweight_cleanup()
73 74 75 76
        surface = cairo.ImageSurface.create_from_png(self.filename)
        surface.write_to_png(self.output_filename)
        return True

77

jvoisin's avatar
jvoisin committed
78 79
class GIFParser(exiftool.ExiftoolParser):
    mimetypes = {'image/gif'}
80
    meta_allowlist = {'AnimationIterations', 'BackgroundColor', 'BitsPerPixel',
jvoisin's avatar
jvoisin committed
81 82 83 84 85 86 87 88 89 90 91 92
                      'ColorResolutionDepth', 'Directory', 'Duration',
                      'ExifToolVersion', 'FileAccessDate',
                      'FileInodeChangeDate', 'FileModifyDate', 'FileName',
                      'FilePermissions', 'FileSize', 'FileType',
                      'FileTypeExtension', 'FrameCount', 'GIFVersion',
                      'HasColorMap', 'ImageHeight', 'ImageSize', 'ImageWidth',
                      'MIMEType', 'Megapixels', 'SourceFile',}

    def remove_all(self) -> bool:
        return self._lightweight_cleanup()


93
class GdkPixbufAbstractParser(exiftool.ExiftoolParser):
jvoisin's avatar
jvoisin committed
94
    """ GdkPixbuf can handle a lot of surfaces, so we're rending images on it,
jvoisin's avatar
jvoisin committed
95
        this has the side-effect of completely removing metadata.
jvoisin's avatar
jvoisin committed
96
    """
97 98
    _type = ''

jvoisin's avatar
jvoisin committed
99 100
    def __init__(self, filename):
        super().__init__(filename)
101 102 103 104
        # we can't use imghdr here because of https://bugs.python.org/issue28591
        try:
            GdkPixbuf.Pixbuf.new_from_file(self.filename)
        except GLib.GError:
jvoisin's avatar
jvoisin committed
105 106 107
            raise ValueError

    def remove_all(self) -> bool:
108 109 110
        if self.lightweight_cleaning:
            return self._lightweight_cleanup()

jvoisin's avatar
jvoisin committed
111
        _, extension = os.path.splitext(self.filename)
jvoisin's avatar
jvoisin committed
112
        pixbuf = GdkPixbuf.Pixbuf.new_from_file(self.filename)
113
        if extension.lower() == '.jpg':
114
            extension = '.jpeg'  # gdk is picky
115 116
        elif extension.lower() == '.tif':
            extension = '.tiff'  # gdk is picky
117 118 119 120 121
        try:
            pixbuf.savev(self.output_filename, type=extension[1:],
                         option_keys=[], option_values=[])
        except GLib.GError:  # pragma: no cover
            return False
jvoisin's avatar
jvoisin committed
122
        return True
jvoisin's avatar
jvoisin committed
123 124 125


class JPGParser(GdkPixbufAbstractParser):
126
    _type = 'jpeg'
127
    mimetypes = {'image/jpeg'}
128
    meta_allowlist = {'SourceFile', 'ExifToolVersion', 'FileName',
jvoisin's avatar
jvoisin committed
129 130 131 132 133 134 135
                      'Directory', 'FileSize', 'FileModifyDate',
                      'FileAccessDate', "FileInodeChangeDate",
                      'FilePermissions', 'FileType', 'FileTypeExtension',
                      'MIMEType', 'ImageWidth', 'ImageSize', 'BitsPerSample',
                      'ColorComponents', 'EncodingProcess', 'JFIFVersion',
                      'ResolutionUnit', 'XResolution', 'YCbCrSubSampling',
                      'YResolution', 'Megapixels', 'ImageHeight'}
jvoisin's avatar
jvoisin committed
136 137 138


class TiffParser(GdkPixbufAbstractParser):
139
    _type = 'tiff'
jvoisin's avatar
jvoisin committed
140
    mimetypes = {'image/tiff'}
141
    meta_allowlist = {'Compression', 'ExifByteOrder', 'ExtraSamples',
jvoisin's avatar
jvoisin committed
142 143 144 145 146 147 148 149
                      'FillOrder', 'PhotometricInterpretation',
                      'PlanarConfiguration', 'RowsPerStrip', 'SamplesPerPixel',
                      'StripByteCounts', 'StripOffsets', 'BitsPerSample',
                      'Directory', 'ExifToolVersion', 'FileAccessDate',
                      'FileInodeChangeDate', 'FileModifyDate', 'FileName',
                      'FilePermissions', 'FileSize', 'FileType',
                      'FileTypeExtension', 'ImageHeight', 'ImageSize',
                      'ImageWidth', 'MIMEType', 'Megapixels', 'SourceFile'}
jvoisin's avatar
jvoisin committed
150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169

class PPMParser(abstract.AbstractParser):
    mimetypes = {'image/x-portable-pixmap'}

    def get_meta(self) -> Dict[str, Union[str, dict]]:
        meta = {}  # type: Dict[str, Union[str, Dict[Any, Any]]]
        with open(self.filename) as f:
            for idx, line in enumerate(f):
                if line.lstrip().startswith('#'):
                    meta[str(idx)] = line.lstrip().rstrip()
        return meta

    def remove_all(self) -> bool:
        with open(self.filename) as fin:
            with open(self.output_filename, 'w') as fout:
                for line in fin:
                    if not line.lstrip().startswith('#'):
                        line = re.sub(r"\s+", "", line, flags=re.UNICODE)
                        fout.write(line)
        return True