def identify(path: str) -> ImageData: import json q = '{"fmt":"%m","canvas":"%g","transparency":"%A","gap":"%T","index":"%p","size":"%wx%h","dpi":"%xx%y","dispose":"%D"},' exe = find_exe('magick') if exe: cmd = [exe, 'identify'] else: cmd = ['identify'] p = run_imagemagick(path, cmd + ['-format', q, '--', path]) data = json.loads(b'[' + p.stdout.rstrip(b',') + b']') first = data[0] frames = list(map(Frame, data)) image_fmt = first['fmt'].lower() if image_fmt == 'gif' and not any(f.gap > 0 for f in frames): # Some broken GIF images have all zero gaps, browsers with their usual # idiot ideas render these with a default 100ms gap https://bugzilla.mozilla.org/show_bug.cgi?id=125137 # Browsers actually force a 100ms gap at any zero gap frame, but that # just means it is impossible to deliberately use zero gap frames for # sophisticated blending, so we dont do that. for f in frames: f.gap = 100 mode = 'rgb' for f in frames: if f.mode == 'rgba': mode = 'rgba' break return ImageData(image_fmt, frames[0].canvas_width, frames[0].canvas_height, mode, frames)
def convert(path: str, m: ImageData, available_width: int, available_height: int, scale_up: bool, tdir: Optional[str] = None) -> Tuple[str, int, int]: from tempfile import NamedTemporaryFile width, height = m.width, m.height exe = find_exe('convert') if exe is None: raise OSError( 'Failed to find the ImageMagick convert executable, make sure it is present in PATH' ) cmd = [exe, '-background', 'none', '--', path] scaled = False if scale_up: if width < available_width: r = available_width / width width, height = available_width, int(height * r) scaled = True if scaled or width > available_width or height > available_height: width, height = fit_image(width, height, available_width, available_height) cmd += ['-resize', '{}x{}!'.format(width, height)] cmd += ['-depth', '8'] with NamedTemporaryFile(prefix='icat-', suffix='.' + m.mode, delete=False, dir=tdir) as outfile: run_imagemagick(path, cmd + [outfile.name]) # ImageMagick sometimes generated rgba images smaller than the specified # size. See https://github.com/kovidgoyal/kitty/issues/276 for examples sz = os.path.getsize(outfile.name) bytes_per_pixel = 3 if m.mode == 'rgb' else 4 expected_size = bytes_per_pixel * width * height if sz < expected_size: missing = expected_size - sz if missing % (bytes_per_pixel * width) != 0: raise ConvertFailed( path, 'ImageMagick failed to convert {} correctly,' ' it generated {} < {} of data (w={}, h={}, bpp={})'.format( path, sz, expected_size, width, height, bytes_per_pixel)) height -= missing // (bytes_per_pixel * width) return outfile.name, width, height
def identify(path: str) -> ImageData: import json q = '{"fmt":"%m","canvas":"%g","transparency":"%A","gap":"%T","index":"%p","size":"%wx%h","dpi":"%xx%y","dispose":"%D"},' exe = find_exe('magick') if exe: cmd = [exe, 'identify'] else: cmd = ['identify'] p = run_imagemagick(path, cmd + ['-format', q, '--', path]) data = json.loads(b'[' + p.stdout.rstrip(b',') + b']') first = data[0] frames = list(map(Frame, data)) mode = 'rgb' for f in frames: if f.mode == 'rgba': mode = 'rgba' break return ImageData(first['fmt'].lower(), frames[0].canvas_width, frames[0].canvas_height, mode, frames)
def render_image( path: str, output_prefix: str, m: ImageData, available_width: int, available_height: int, scale_up: bool, only_first_frame: bool = False ) -> RenderedImage: import tempfile has_multiple_frames = len(m) > 1 get_multiple_frames = has_multiple_frames and not only_first_frame exe = find_exe('magick') if exe: cmd = [exe, 'convert'] else: exe = find_exe('convert') if exe is None: raise OSError('Failed to find the ImageMagick convert executable, make sure it is present in PATH') cmd = [exe] cmd += ['-background', 'none', '--', path] if only_first_frame and has_multiple_frames: cmd[-1] += '[0]' scaled = False width, height = m.width, m.height if scale_up: if width < available_width: r = available_width / width width, height = available_width, int(height * r) scaled = True if scaled or width > available_width or height > available_height: width, height = fit_image(width, height, available_width, available_height) resize_cmd = ['-resize', '{}x{}!'.format(width, height)] if get_multiple_frames: # we have to coalesce, resize and de-coalesce all frames resize_cmd = ['-coalesce'] + resize_cmd + ['-deconstruct'] cmd += resize_cmd cmd += ['-depth', '8', '-set', 'filename:f', '%w-%h-%g-%p'] ans = RenderedImage(m.fmt, width, height, m.mode) if only_first_frame: ans.frames = [Frame(m.frames[0])] else: ans.frames = list(map(Frame, m.frames)) bytes_per_pixel = 3 if m.mode == 'rgb' else 4 def check_resize(frame: Frame) -> None: # ImageMagick sometimes generates RGBA images smaller than the specified # size. See https://github.com/kovidgoyal/kitty/issues/276 for examples sz = os.path.getsize(frame.path) expected_size = bytes_per_pixel * frame.width * frame.height if sz < expected_size: missing = expected_size - sz if missing % (bytes_per_pixel * width) != 0: raise ConvertFailed( path, 'ImageMagick failed to convert {} correctly,' ' it generated {} < {} of data (w={}, h={}, bpp={})'.format( path, sz, expected_size, frame.width, frame.height, bytes_per_pixel)) frame.height -= missing // (bytes_per_pixel * frame.width) if frame.index == 0: ans.height = frame.height ans.width = frame.width with tempfile.TemporaryDirectory(dir=os.path.dirname(output_prefix)) as tdir: output_template = os.path.join(tdir, f'im-%[filename:f].{m.mode}') if get_multiple_frames: cmd.append('+adjoin') run_imagemagick(path, cmd + [output_template]) unseen = {x.index for x in m} for x in os.listdir(tdir): try: parts = x.split('.', 1)[0].split('-') index = int(parts[-1]) unseen.discard(index) f = ans.frames[index] f.width, f.height = map(positive_int, parts[1:3]) sz, pos = parts[3].split('+', 1) f.canvas_width, f.canvas_height = map(positive_int, sz.split('x', 1)) f.canvas_x, f.canvas_y = map(int, pos.split('+', 1)) except Exception: raise OutdatedImageMagick(f'Unexpected output filename: {x!r} produced by ImageMagick command: {last_imagemagick_cmd}') f.path = output_prefix + f'-{index}.{m.mode}' os.rename(os.path.join(tdir, x), f.path) check_resize(f) f = ans.frames[0] if f.width != ans.width or f.height != ans.height: with open(f.path, 'r+b') as ff: data = ff.read() ff.seek(0) ff.truncate() cd = create_canvas(data, f.width, f.canvas_x, f.canvas_y, ans.width, ans.height, 3 if ans.mode == 'rgb' else 4) ff.write(cd) if get_multiple_frames: if unseen: raise ConvertFailed(path, f'Failed to render {len(unseen)} out of {len(m)} frames of animation') elif not ans.frames[0].path: raise ConvertFailed(path, 'Failed to render image') return ans