images.py 6.31 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)
jvoisin's avatar
jvoisin committed
66
        except MemoryError:  # pragma: no cover
67 68
            raise ValueError

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

76

jvoisin's avatar
jvoisin committed
77 78
class GIFParser(exiftool.ExiftoolParser):
    mimetypes = {'image/gif'}
79
    meta_allowlist = {'AnimationIterations', 'BackgroundColor', 'BitsPerPixel',
jvoisin's avatar
jvoisin committed
80 81 82 83 84 85 86 87 88 89 90 91
                      '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()


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

jvoisin's avatar
jvoisin committed
98 99
    def __init__(self, filename):
        super().__init__(filename)
100 101 102 103
        # 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
104 105 106
            raise ValueError

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

jvoisin's avatar
jvoisin committed
110
        _, extension = os.path.splitext(self.filename)
jvoisin's avatar
jvoisin committed
111
        pixbuf = GdkPixbuf.Pixbuf.new_from_file(self.filename)
112
        if extension.lower() == '.jpg':
113
            extension = '.jpeg'  # gdk is picky
114
        pixbuf.savev(self.output_filename, type=extension[1:], option_keys=[], option_values=[])
jvoisin's avatar
jvoisin committed
115
        return True
jvoisin's avatar
jvoisin committed
116 117 118


class JPGParser(GdkPixbufAbstractParser):
119
    _type = 'jpeg'
120
    mimetypes = {'image/jpeg'}
121
    meta_allowlist = {'SourceFile', 'ExifToolVersion', 'FileName',
jvoisin's avatar
jvoisin committed
122 123 124 125 126 127 128
                      '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
129 130 131


class TiffParser(GdkPixbufAbstractParser):
132
    _type = 'tiff'
jvoisin's avatar
jvoisin committed
133
    mimetypes = {'image/tiff'}
134
    meta_allowlist = {'Compression', 'ExifByteOrder', 'ExtraSamples',
jvoisin's avatar
jvoisin committed
135 136 137 138 139 140 141 142
                      '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
143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162

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