1'''image_tools.py - Various image manipulations.''' 2 3import binascii 4from functools import reduce 5from io import BytesIO 6import os 7import re 8import sys 9import operator 10 11from gi.repository import GdkPixbuf, Gio, GLib 12from PIL import Image 13from PIL import ImageEnhance 14from PIL import ImageOps 15from PIL import ImageSequence 16 17from mcomix import anime_tools 18from mcomix import constants 19from mcomix import log 20from mcomix import tools 21from mcomix.lib import reader 22from mcomix.preferences import prefs 23 24if tools.use_gui(): 25 from gi.repository import Gdk, Gtk 26 27 # Fallback pixbuf for missing images. 28 MISSING_IMAGE_ICON = None 29 30 _missing_icon_dialog = Gtk.Dialog() 31 _missing_icon_pixbuf = _missing_icon_dialog.render_icon( 32 Gtk.STOCK_MISSING_IMAGE, Gtk.IconSize.LARGE_TOOLBAR 33 ) 34 MISSING_IMAGE_ICON = _missing_icon_pixbuf 35 assert MISSING_IMAGE_ICON 36 37 GTK_GDK_COLOR_BLACK = Gdk.color_parse('black') 38 GTK_GDK_COLOR_WHITE = Gdk.color_parse('white') 39 40def _getexif(im): 41 exif={} 42 try: 43 exif.update(im.getexif()) 44 except AttributeError: 45 pass 46 if exif: 47 return exif 48 49 # Exif of PNG is still buggy in Pillow 6.0.0 50 try: 51 l1,l2,size,*lines=im.info.get('Raw profile type exif').splitlines() 52 if l2!='exif': 53 # Not valid Exif data. 54 return {} 55 size=int(size) 56 data=binascii.unhexlify(''.join(lines)) 57 if len(data)!=size: 58 # Size not match. 59 return {} 60 im.info['exif']=data 61 except: 62 # Not valid Exif data. 63 return {} 64 65 # load Exif again 66 try: 67 exif.update(im.getexif()) 68 except AttributeError: 69 pass 70 return exif 71 72def rotate_pixbuf(src, rotation): 73 rotation %= 360 74 if 0 == rotation: 75 return src 76 if 90 == rotation: 77 return src.rotate_simple(GdkPixbuf.PixbufRotation.CLOCKWISE) 78 if 180 == rotation: 79 return src.rotate_simple(GdkPixbuf.PixbufRotation.UPSIDEDOWN) 80 if 270 == rotation: 81 return src.rotate_simple(GdkPixbuf.PixbufRotation.COUNTERCLOCKWISE) 82 raise ValueError('unsupported rotation: %s' % rotation) 83 84def get_fitting_size(source_size, target_size, 85 keep_ratio=True, scale_up=False): 86 ''' Return a scaled version of <source_size> 87 small enough to fit in <target_size>. 88 89 Both <source_size> and <target_size> 90 must be (width, height) tuples. 91 92 If <keep_ratio> is True, aspect ratio is kept. 93 94 If <scale_up> is True, <source_size> is scaled up 95 when smaller than <target_size>. 96 ''' 97 width, height = target_size 98 src_width, src_height = source_size 99 if not scale_up and src_width <= width and src_height <= height: 100 width, height = src_width, src_height 101 else: 102 if keep_ratio: 103 if float(src_width) / width > float(src_height) / height: 104 height = int(max(src_height * width / src_width, 1)) 105 else: 106 width = int(max(src_width * height / src_height, 1)) 107 return (width, height) 108 109def trans_pixbuf(src,flip=False,flop=False): 110 if is_animation(src): 111 return anime_tools.frame_executor( 112 src, trans_pixbuf, 113 kwargs=dict(flip=flip, flop=flop) 114 ) 115 if flip: src = src.flip(horizontal=False) 116 if flop: src = src.flip(horizontal=True) 117 return src 118 119def fit_pixbuf_to_rectangle(src, rect, rotation): 120 if is_animation(src): 121 return anime_tools.frame_executor( 122 src, fit_pixbuf_to_rectangle, 123 args=(rect, rotation) 124 ) 125 return fit_in_rectangle(src, rect[0], rect[1], 126 rotation=rotation, 127 keep_ratio=False, 128 scale_up=True) 129 130def fit_in_rectangle(src, width, height, keep_ratio=True, scale_up=False, 131 rotation=0, scaling_quality=None, pil_filter=None): 132 '''Scale (and return) a pixbuf so that it fits in a rectangle with 133 dimensions <width> x <height>. A negative <width> or <height> 134 means an unbounded dimension - both cannot be negative. 135 136 If <rotation> is 90, 180 or 270 we rotate <src> first so that the 137 rotated pixbuf is fitted in the rectangle. 138 139 Unless <scale_up> is True we don't stretch images smaller than the 140 given rectangle. 141 142 If <keep_ratio> is True, the image ratio is kept, and the result 143 dimensions may be smaller than the target dimensions. 144 145 If <src> has an alpha channel it gets a checkboard background. 146 ''' 147 # "Unbounded" really means "bounded to 100000 px" - for simplicity. 148 # MComix would probably choke on larger images anyway. 149 if width < 0: 150 width = 100000 151 elif height < 0: 152 height = 100000 153 width = max(width, 1) 154 height = max(height, 1) 155 156 rotation %= 360 157 if rotation not in (0, 90, 180, 270): 158 raise ValueError('unsupported rotation: %s' % rotation) 159 if rotation in (90, 270): 160 width, height = height, width 161 162 if scaling_quality is None: 163 scaling_quality = prefs['scaling quality'] 164 165 if pil_filter is None: 166 pil_filter = prefs['pil scaling filter'] 167 168 src_width = src.get_width() 169 src_height = src.get_height() 170 171 width, height = get_fitting_size((src_width, src_height), 172 (width, height), 173 keep_ratio=keep_ratio, 174 scale_up=scale_up) 175 176 if (width, height) != (src_width, src_height) and pil_filter > -1: 177 # scale by PIL interpolation filter 178 src = pil_to_pixbuf(pixbuf_to_pil(src).resize( 179 [width,height], resample=pil_filter)) 180 src_width = src.get_width() 181 src_height = src.get_height() 182 assert (width, height) == (src_width, src_height),'PIL resize bug' 183 184 if src.get_has_alpha(): 185 if prefs['checkered bg for transparent images']: 186 check_size, color1, color2 = 8, 0x777777, 0x999999 187 else: 188 check_size, color1, color2 = 1024, 0xFFFFFF, 0xFFFFFF 189 if (width, height) == (src_width, src_height): 190 # Using anything other than nearest interpolation will result in a 191 # modified image if no resizing takes place (even if it's opaque). 192 scaling_quality = GdkPixbuf.InterpType.NEAREST 193 src = src.composite_color_simple(width, height, scaling_quality, 194 255, check_size, color1, color2) 195 elif (width, height) != (src_width, src_height): 196 src = src.scale_simple(width, height, scaling_quality) 197 198 src = rotate_pixbuf(src, rotation) 199 200 return src 201 202def add_border(pixbuf, thickness, colour=0x000000FF): 203 '''Return a pixbuf from <pixbuf> with a <thickness> px border of 204 <colour> added. 205 ''' 206 canvas = GdkPixbuf.Pixbuf.new(GdkPixbuf.Colorspace.RGB, True, 8, 207 pixbuf.get_width() + thickness * 2, 208 pixbuf.get_height() + thickness * 2) 209 canvas.fill(colour) 210 pixbuf.copy_area(0, 0, pixbuf.get_width(), pixbuf.get_height(), 211 canvas, thickness, thickness) 212 return canvas 213 214 215def get_most_common_edge_color(pixbufs, edge=2): 216 '''Return the most commonly occurring pixel value along the four edges 217 of <pixbuf>. The return value is a sequence, (r, g, b), with 16 bit 218 values. If <pixbuf> is a tuple, the edges will be computed from 219 both the left and the right image. 220 221 Note: This could be done more cleanly with subpixbuf(), but that 222 doesn't work as expected together with get_pixels(). 223 ''' 224 225 def group_colors(colors, steps=10): 226 ''' This rounds a list of colors in C{colors} to the next nearest value, 227 i.e. 128, 83, 10 becomes 130, 85, 10 with C{steps}=5. This compensates for 228 dirty colors where no clear dominating color can be made out. 229 230 @return: The color that appears most often in the prominent group.''' 231 232 # Start group 233 group = (0, 0, 0) 234 # List of (count, color) pairs, group contains most colors 235 colors_in_prominent_group = [] 236 color_count_in_prominent_group = 0 237 # List of (count, color) pairs, current color group 238 colors_in_group = [] 239 color_count_in_group = 0 240 241 for count, color in colors: 242 243 # Round color 244 rounded = [0] * len(color) 245 for i, color_value in enumerate(color): 246 if steps % 2 == 0: 247 middle = steps // 2 248 else: 249 middle = steps // 2 + 1 250 251 remainder = color_value % steps 252 if remainder >= middle: 253 color_value = color_value + (steps - remainder) 254 else: 255 color_value = color_value - remainder 256 257 rounded[i] = min(255, max(0, color_value)) 258 259 # Change prominent group if necessary 260 if rounded == group: 261 # Color still fits in the previous color group 262 colors_in_group.append((count, color)) 263 color_count_in_group += count 264 else: 265 # Color group changed, check if current group has more colors 266 # than last group 267 if color_count_in_group > color_count_in_prominent_group: 268 colors_in_prominent_group = colors_in_group 269 color_count_in_prominent_group = color_count_in_group 270 271 group = rounded 272 colors_in_group = [ (count, color) ] 273 color_count_in_group = count 274 275 # Cleanup if only one edge color group was found 276 if color_count_in_group > color_count_in_prominent_group: 277 colors_in_prominent_group = colors_in_group 278 279 colors_in_prominent_group.sort(key=operator.itemgetter(0), reverse=True) 280 # List is now sorted by color count, first color appears most often 281 return colors_in_prominent_group[0][1] 282 283 def get_edge_pixbuf(pixbuf, side, edge): 284 ''' Returns a pixbuf corresponding to the side passed in <side>. 285 Valid sides are 'left', 'right', 'top', 'bottom'. ''' 286 pixbuf = static_image(pixbuf) 287 width = pixbuf.get_width() 288 height = pixbuf.get_height() 289 edge = min(edge, width, height) 290 291 subpix = GdkPixbuf.Pixbuf.new(GdkPixbuf.Colorspace.RGB, 292 pixbuf.get_has_alpha(), 8, edge, height) 293 if side == 'left': 294 pixbuf.copy_area(0, 0, edge, height, subpix, 0, 0) 295 elif side == 'right': 296 pixbuf.copy_area(width - edge, 0, edge, height, subpix, 0, 0) 297 elif side == 'top': 298 pixbuf.copy_area(0, 0, width, edge, subpix, 0, 0) 299 elif side == 'bottom': 300 pixbuf.copy_area(0, height - edge, width, edge, subpix, 0, 0) 301 else: 302 assert False, 'Invalid edge side' 303 304 return subpix 305 306 if not pixbufs: 307 return (0, 0, 0) 308 309 if not isinstance(pixbufs, (tuple, list)): 310 left_edge = get_edge_pixbuf(pixbufs, 'left', edge) 311 right_edge = get_edge_pixbuf(pixbufs, 'right', edge) 312 else: 313 assert len(pixbufs) == 2, 'Expected two pages in list' 314 left_edge = get_edge_pixbuf(pixbufs[0], 'left', edge) 315 right_edge = get_edge_pixbuf(pixbufs[1], 'right', edge) 316 317 # Find all edge colors. Color count is separate for all four edges 318 ungrouped_colors = [] 319 for edge in (left_edge, right_edge): 320 im = pixbuf_to_pil(edge) 321 ungrouped_colors.extend(im.getcolors(im.size[0] * im.size[1])) 322 323 # Sum up colors from all edges 324 ungrouped_colors.sort(key=operator.itemgetter(1)) 325 most_used = group_colors(ungrouped_colors) 326 return [color/255 for color in most_used] 327 328def pil_to_pixbuf(im, keep_orientation=False): 329 '''Return a pixbuf created from the PIL <im>.''' 330 if im.mode.startswith('RGB'): 331 has_alpha = im.mode == 'RGBA' 332 elif im.mode in ('LA', 'P'): 333 has_alpha = True 334 else: 335 has_alpha = False 336 target_mode = 'RGBA' if has_alpha else 'RGB' 337 if im.mode != target_mode: 338 im = im.convert(target_mode) 339 pixbuf = GdkPixbuf.Pixbuf.new_from_bytes( 340 GLib.Bytes.new(im.tobytes()), GdkPixbuf.Colorspace.RGB, 341 has_alpha, 8, 342 im.size[0], im.size[1], 343 (4 if has_alpha else 3) * im.size[0] 344 ) 345 if keep_orientation: 346 # Keep orientation metadata. 347 orientation = _getexif(im).get(274, None) 348 if orientation is not None: 349 setattr(pixbuf, 'orientation', str(orientation)) 350 return pixbuf 351 352def pixbuf_to_pil(pixbuf): 353 '''Return a PIL image created from <pixbuf>.''' 354 dimensions = pixbuf.get_width(), pixbuf.get_height() 355 stride = pixbuf.get_rowstride() 356 pixels = pixbuf.get_pixels() 357 mode = 'RGBA' if pixbuf.get_has_alpha() else 'RGB' 358 im = Image.frombuffer(mode, dimensions, pixels, 'raw', mode, stride, 1) 359 return im 360 361def is_animation(pixbuf): 362 return isinstance(pixbuf, GdkPixbuf.PixbufAnimation) 363 364def disable_transform(pixbuf): 365 if is_animation(pixbuf): 366 if not hasattr(pixbuf,'_framebuffer'): 367 return True 368 if not prefs['animation transform']: 369 return True 370 return False 371 372def static_image(pixbuf): 373 ''' Returns a non-animated version of the specified pixbuf. ''' 374 if is_animation(pixbuf): 375 return pixbuf.get_static_image() 376 return pixbuf 377 378def unwrap_image(image): 379 ''' Returns an object that contains the image data based on 380 gtk.Image.get_storage_type or None if image is None or image.get_storage_type 381 returns Gtk.ImageType.EMPTY. ''' 382 if image is None: 383 return None 384 t = image.get_storage_type() 385 if t == Gtk.ImageType.EMPTY: 386 return None 387 if t == Gtk.ImageType.PIXBUF: 388 return image.get_pixbuf() 389 if t == Gtk.ImageType.ANIMATION: 390 return image.get_animation() 391 if t == Gtk.ImageType.PIXMAP: 392 return image.get_pixmap() 393 if t == Gtk.ImageType.IMAGE: 394 return image.get_image() 395 if t == Gtk.ImageType.STOCK: 396 return image.get_stock() 397 if t == Gtk.ImageType.ICON_SET: 398 return image.get_icon_set() 399 raise ValueError() 400 401def set_from_pixbuf(image, pixbuf): 402 if is_animation(pixbuf): 403 return image.set_from_animation(pixbuf) 404 else: 405 return image.set_from_pixbuf(pixbuf) 406 407def load_animation(im): 408 if im.format=='GIF' and im.mode=='P': 409 # TODO: Pillow has bug with gif animation 410 # https://github.com/python-pillow/Pillow/labels/GIF 411 raise NotImplementedError('Pillow has bug with gif animation, ' 412 'fallback to GdkPixbuf') 413 anime=anime_tools.AnimeFrameBuffer(im.n_frames,loop=im.info['loop']) 414 background=im.info.get('background',None) 415 if isinstance(background,tuple): 416 color=0 417 for n,c in enumerate(background): 418 color|=c<<n*8 419 background=color 420 frameiter=ImageSequence.Iterator(im) 421 for n,frame in enumerate(frameiter): 422 anime.add_frame(n,pil_to_pixbuf(frame), 423 int(frame.info.get('duration',0)), 424 background=background) 425 return anime.create_animation() 426 427def load_pixbuf(path): 428 ''' Loads a pixbuf from a given image file. ''' 429 enable_anime = prefs['animation mode'] != constants.ANIMATION_DISABLED 430 try: 431 with reader.LockedFileIO(path) as fio: 432 with Image.open(fio) as im: 433 # make sure n_frames loaded 434 im.load() 435 if enable_anime and getattr(im,'is_animated',False): 436 return load_animation(im) 437 return pil_to_pixbuf(im, keep_orientation=True) 438 except: 439 pass 440 if enable_anime: 441 pixbuf = GdkPixbuf.PixbufAnimation.new_from_file(path) 442 if pixbuf.is_static_image(): 443 return pixbuf.get_static_image() 444 return pixbuf 445 return GdkPixbuf.Pixbuf.new_from_file(path) 446 447def load_pixbuf_size(path, width, height): 448 ''' Loads a pixbuf from a given image file and scale it to fit 449 inside (width, height). ''' 450 try: 451 with reader.LockedFileIO(path) as fio: 452 with Image.open(fio) as im: 453 im.thumbnail((width, height), resample=Image.BOX) 454 return pil_to_pixbuf(im, keep_orientation=True) 455 except: 456 info, image_width, image_height = GdkPixbuf.Pixbuf.get_file_info(path) 457 # If we could not get the image info, still try to load 458 # the image to let GdkPixbuf raise the appropriate exception. 459 if not info: 460 pixbuf = GdkPixbuf.Pixbuf.new_from_file(path) 461 # Don't upscale if smaller than target dimensions! 462 if image_width <= width and image_height <= height: 463 width, height = image_width, image_height 464 return GdkPixbuf.Pixbuf.new_from_file_at_size(path, width, height) 465 466def load_pixbuf_data(imgdata): 467 ''' Loads a pixbuf from the data passed in <imgdata>. ''' 468 try: 469 with Image.open(BytesIO(imgdata)) as im: 470 return pil_to_pixbuf(im, keep_orientation=True) 471 except: 472 pass 473 loader = GdkPixbuf.PixbufLoader() 474 loader.write(imgdata) 475 loader.close() 476 return loader.get_pixbuf() 477 478def enhance(pixbuf, brightness=1.0, contrast=1.0, saturation=1.0, 479 sharpness=1.0, autocontrast=False): 480 '''Return a modified pixbuf from <pixbuf> where the enhancement operations 481 corresponding to each argument has been performed. A value of 1.0 means 482 no change. If <autocontrast> is True it overrides the <contrast> value, 483 but only if the image mode is supported by ImageOps.autocontrast (i.e. 484 it is L or RGB.) 485 ''' 486 if is_animation(pixbuf): 487 return anime_tools.frame_executor( 488 pixbuf, enhance, 489 kwargs=dict( 490 brightness=brightness, contrast=contrast, 491 saturation=saturation, sharpness=1.0, 492 autocontrast=False 493 ) 494 ) 495 im = pixbuf_to_pil(pixbuf) 496 if brightness != 1.0: 497 im = ImageEnhance.Brightness(im).enhance(brightness) 498 if autocontrast and im.mode in ('L', 'RGB'): 499 im = ImageOps.autocontrast(im, cutoff=0.1) 500 elif contrast != 1.0: 501 im = ImageEnhance.Contrast(im).enhance(contrast) 502 if saturation != 1.0: 503 im = ImageEnhance.Color(im).enhance(saturation) 504 if sharpness != 1.0: 505 im = ImageEnhance.Sharpness(im).enhance(sharpness) 506 return pil_to_pixbuf(im) 507 508def get_implied_rotation(pixbuf): 509 '''Return the implied rotation in degrees: 0, 90, 180, or 270. 510 511 The implied rotation is the angle (in degrees) that the raw pixbuf should 512 be rotated in order to be displayed "correctly". E.g. a photograph taken 513 by a camera that is held sideways might store this fact in its Exif data, 514 and the pixbuf loader will set the orientation option correspondingly. 515 ''' 516 pixbuf = static_image(pixbuf) 517 orientation = getattr(pixbuf, 'orientation', None) 518 if orientation is None: 519 orientation = pixbuf.get_option('orientation') 520 if orientation == '3': 521 return 180 522 elif orientation == '6': 523 return 90 524 elif orientation == '8': 525 return 270 526 return 0 527 528def combine_pixbufs( pixbuf1, pixbuf2, are_in_manga_mode ): 529 if are_in_manga_mode: 530 r_source_pixbuf = pixbuf1 531 l_source_pixbuf = pixbuf2 532 else: 533 l_source_pixbuf = pixbuf1 534 r_source_pixbuf = pixbuf2 535 536 has_alpha = False 537 538 if l_source_pixbuf.get_property('has-alpha') or \ 539 r_source_pixbuf.get_property('has-alpha'): 540 has_alpha = True 541 542 bits_per_sample = 8 543 544 l_source_pixbuf_width = l_source_pixbuf.get_property('width') 545 r_source_pixbuf_width = r_source_pixbuf.get_property('width') 546 547 l_source_pixbuf_height = l_source_pixbuf.get_property('height') 548 r_source_pixbuf_height = r_source_pixbuf.get_property('height') 549 550 new_width = l_source_pixbuf_width + r_source_pixbuf_width 551 552 new_height = max(l_source_pixbuf_height, r_source_pixbuf_height) 553 554 new_pix_buf = GdkPixbuf.Pixbuf.new(colorspace=GdkPixbuf.Colorspace.RGB, 555 has_alpha=has_alpha, 556 bits_per_sample=bits_per_sample, 557 width=new_width, height=new_height) 558 559 l_source_pixbuf.copy_area(0, 0, l_source_pixbuf_width, 560 l_source_pixbuf_height, 561 new_pix_buf, 0, 0) 562 563 r_source_pixbuf.copy_area(0, 0, r_source_pixbuf_width, 564 r_source_pixbuf_height, 565 new_pix_buf, l_source_pixbuf_width, 0) 566 567 return new_pix_buf 568 569def convert_rgb16list_to_rgba8int(c): 570 return 0x000000FF | (c[0] >> 8 << 24) | (c[1] >> 8 << 16) | (c[2] >> 8 << 8) 571 572def rgb_to_y_601(color): 573 return color[0] * 0.299 + color[1] * 0.587 + color[2] * 0.114 574 575def text_color_for_background_color(bgcolor): 576 return GTK_GDK_COLOR_BLACK if rgb_to_y_601(bgcolor) >= \ 577 65535.0 / 2.0 else GTK_GDK_COLOR_WHITE 578 579def get_image_info(path): 580 '''Return image informations: 581 (format, width, height) 582 ''' 583 info = None 584 try: 585 with reader.LockedFileIO(path) as fio: 586 with Image.open(fio) as im: 587 return (im.format,) + im.size 588 except: 589 info = GdkPixbuf.Pixbuf.get_file_info(path) 590 if info[0] is None: 591 info = None 592 else: 593 info = info[0].get_name().upper(), info[1], info[2] 594 if info is None: 595 info = (_('Unknown filetype'), 0, 0) 596 return info 597 598SUPPORTED_IMAGE_EXTS=set() 599SUPPORTED_IMAGE_MIMES=set() 600SUPPORTED_IMAGE_FORMATS={} 601 602def init_supported_formats(): 603 # formats supported by PIL 604 # Make sure all supported formats are registered. 605 Image.init() 606 for ext,name in Image.EXTENSION.items(): 607 fmt=SUPPORTED_IMAGE_FORMATS.setdefault(name,(set(),set())) 608 fmt[1].add(ext.lower()) 609 mime=Image.MIME.get( 610 name, Gio.content_type_guess(filename='file'+ext)[0]).lower() 611 if mime and mime != 'application/octet-stream': 612 fmt[0].add(mime) 613 614 # formats supported by gdk-pixbuf 615 for gdkfmt in GdkPixbuf.Pixbuf.get_formats(): 616 fmt=SUPPORTED_IMAGE_FORMATS.setdefault( 617 gdkfmt.get_name().upper(),(set(),set())) 618 for m in map(lambda s:s.lower(),gdkfmt.get_mime_types()): 619 fmt[0].add(m) 620 # get_extensions() return extensions without '.' 621 for e in map(lambda s:'.'+s.lower(),gdkfmt.get_extensions()): 622 fmt[1].add(e) 623 m = Gio.content_type_guess(filename='file'+e)[0].lower() 624 if m and m != 'application/octet-stream': 625 fmt[0].add(m) 626 627 # cache a supported extensions list 628 for mimes,exts in SUPPORTED_IMAGE_FORMATS.values(): 629 SUPPORTED_IMAGE_EXTS.update(exts) 630 SUPPORTED_IMAGE_MIMES.update(mimes) 631 632def get_supported_formats(): 633 if not SUPPORTED_IMAGE_FORMATS: 634 init_supported_formats() 635 return SUPPORTED_IMAGE_FORMATS 636 637def is_image_file(path, check_mimetype=False): 638 # if check_mimetype is True, 639 # read starting bytes and using Gio.content_type_guess 640 # to guess if path is supported, ignoring file extension. 641 if not SUPPORTED_IMAGE_FORMATS: 642 init_supported_formats() 643 if prefs['check image mimetype'] and check_mimetype and os.path.isfile(path): 644 with open(path, mode='rb') as fd: 645 magic = fd.read(10) 646 mime, uncertain = Gio.content_type_guess(data=magic) 647 return mime.lower() in SUPPORTED_IMAGE_MIMES 648 return path.lower().endswith(tuple(SUPPORTED_IMAGE_EXTS)) 649 650# vim: expandtab:sw=4:ts=4 651