images.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607
  1. #!/usr/bin/env python
  2. # License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
  3. import codecs
  4. import os
  5. import sys
  6. from base64 import standard_b64encode
  7. from collections import defaultdict, deque
  8. from collections.abc import Callable, Iterator, Sequence
  9. from contextlib import suppress
  10. from enum import IntEnum
  11. from itertools import count
  12. from typing import Any, ClassVar, DefaultDict, Deque, Generic, Optional, TypeVar, Union, cast
  13. from kitty.conf.utils import positive_float, positive_int
  14. from kitty.fast_data_types import create_canvas
  15. from kitty.typing import GRT_C, CompletedProcess, GRT_a, GRT_d, GRT_f, GRT_m, GRT_o, GRT_t, HandlerType
  16. from kitty.utils import ScreenSize, fit_image, which
  17. from .operations import cursor
  18. try:
  19. fsenc = sys.getfilesystemencoding() or 'utf-8'
  20. codecs.lookup(fsenc)
  21. except Exception:
  22. fsenc = 'utf-8'
  23. class Dispose(IntEnum):
  24. undefined = 0
  25. none = 1
  26. background = 2
  27. previous = 3
  28. class Frame:
  29. gap: int # milliseconds
  30. canvas_width: int
  31. canvas_height: int
  32. width: int
  33. height: int
  34. index: int
  35. xdpi: float
  36. ydpi: float
  37. canvas_x: int
  38. canvas_y: int
  39. mode: str
  40. needs_blend: bool
  41. dimensions_swapped: bool
  42. dispose: Dispose
  43. path: str = ''
  44. def __init__(self, identify_data: Union['Frame', dict[str, str]]):
  45. if isinstance(identify_data, Frame):
  46. for k in Frame.__annotations__:
  47. setattr(self, k, getattr(identify_data, k))
  48. else:
  49. self.gap = max(0, int(identify_data['gap']) * 10)
  50. sz, pos = identify_data['canvas'].split('+', 1)
  51. self.canvas_width, self.canvas_height = map(positive_int, sz.split('x', 1))
  52. self.canvas_x, self.canvas_y = map(int, pos.split('+', 1))
  53. self.width, self.height = map(positive_int, identify_data['size'].split('x', 1))
  54. self.xdpi, self.ydpi = map(positive_float, identify_data['dpi'].split('x', 1))
  55. self.index = positive_int(identify_data['index'])
  56. q = identify_data['transparency'].lower()
  57. self.mode = 'rgba' if q in ('blend', 'true') else 'rgb'
  58. self.needs_blend = q == 'blend'
  59. self.dispose = getattr(Dispose, identify_data['dispose'].lower())
  60. self.dimensions_swapped = identify_data.get('orientation') in ('5', '6', '7', '8')
  61. if self.dimensions_swapped:
  62. self.canvas_width, self.canvas_height = self.canvas_height, self.canvas_width
  63. self.width, self.height = self.height, self.width
  64. def __repr__(self) -> str:
  65. canvas = f'{self.canvas_width}x{self.canvas_height}:{self.canvas_x}+{self.canvas_y}'
  66. geom = f'{self.width}x{self.height}'
  67. return f'Frame(index={self.index}, gap={self.gap}, geom={geom}, canvas={canvas}, dispose={self.dispose.name})'
  68. class ImageData:
  69. def __init__(self, fmt: str, width: int, height: int, mode: str, frames: list[Frame]):
  70. self.width, self.height, self.fmt, self.mode = width, height, fmt, mode
  71. self.transmit_fmt: GRT_f = (24 if self.mode == 'rgb' else 32)
  72. self.frames = frames
  73. def __len__(self) -> int:
  74. return len(self.frames)
  75. def __iter__(self) -> Iterator[Frame]:
  76. yield from self.frames
  77. def __repr__(self) -> str:
  78. frames = '\n '.join(map(repr, self.frames))
  79. return f'Image(fmt={self.fmt}, mode={self.mode},\n {frames}\n)'
  80. class OpenFailed(ValueError):
  81. def __init__(self, path: str, message: str):
  82. ValueError.__init__(
  83. self, f'Failed to open image: {path} with error: {message}'
  84. )
  85. self.path = path
  86. class ConvertFailed(ValueError):
  87. def __init__(self, path: str, message: str):
  88. ValueError.__init__(
  89. self, f'Failed to convert image: {path} with error: {message}'
  90. )
  91. self.path = path
  92. class NoImageMagick(Exception):
  93. pass
  94. class OutdatedImageMagick(ValueError):
  95. def __init__(self, detailed_error: str):
  96. super().__init__('ImageMagick on this system is too old ImageMagick 7+ required which was first released in 2016')
  97. self.detailed_error = detailed_error
  98. last_imagemagick_cmd: Sequence[str] = ()
  99. def run_imagemagick(path: str, cmd: Sequence[str], keep_stdout: bool = True) -> 'CompletedProcess[bytes]':
  100. global last_imagemagick_cmd
  101. import subprocess
  102. last_imagemagick_cmd = cmd
  103. try:
  104. p = subprocess.run(cmd, stdout=subprocess.PIPE if keep_stdout else subprocess.DEVNULL, stderr=subprocess.PIPE)
  105. except FileNotFoundError:
  106. raise NoImageMagick('ImageMagick is required to process images')
  107. if p.returncode != 0:
  108. raise OpenFailed(path, p.stderr.decode('utf-8'))
  109. return p
  110. def identify(path: str) -> ImageData:
  111. import json
  112. q = (
  113. '{"fmt":"%m","canvas":"%g","transparency":"%A","gap":"%T","index":"%p","size":"%wx%h",'
  114. '"dpi":"%xx%y","dispose":"%D","orientation":"%[EXIF:Orientation]"},'
  115. )
  116. exe = which('magick')
  117. if exe:
  118. cmd = [exe, 'identify']
  119. else:
  120. cmd = ['identify']
  121. p = run_imagemagick(path, cmd + ['-format', q, '--', path])
  122. raw = p.stdout.rstrip(b',')
  123. data = json.loads(b'[' + raw + b']')
  124. first = data[0]
  125. frames = list(map(Frame, data))
  126. image_fmt = first['fmt'].lower()
  127. if image_fmt == 'gif' and not any(f.gap > 0 for f in frames):
  128. # Some broken GIF images have all zero gaps, browsers with their usual
  129. # idiot ideas render these with a default 100ms gap https://bugzilla.mozilla.org/show_bug.cgi?id=125137
  130. # Browsers actually force a 100ms gap at any zero gap frame, but that
  131. # just means it is impossible to deliberately use zero gap frames for
  132. # sophisticated blending, so we dont do that.
  133. for f in frames:
  134. f.gap = 100
  135. mode = 'rgb'
  136. for f in frames:
  137. if f.mode == 'rgba':
  138. mode = 'rgba'
  139. break
  140. return ImageData(image_fmt, frames[0].canvas_width, frames[0].canvas_height, mode, frames)
  141. class RenderedImage(ImageData):
  142. def __init__(self, fmt: str, width: int, height: int, mode: str):
  143. super().__init__(fmt, width, height, mode, [])
  144. def render_image(
  145. path: str, output_prefix: str,
  146. m: ImageData,
  147. available_width: int, available_height: int,
  148. scale_up: bool,
  149. only_first_frame: bool = False,
  150. remove_alpha: str = '',
  151. flip: bool = False, flop: bool = False,
  152. ) -> RenderedImage:
  153. import tempfile
  154. has_multiple_frames = len(m) > 1
  155. get_multiple_frames = has_multiple_frames and not only_first_frame
  156. exe = which('magick')
  157. if exe:
  158. cmd = [exe, 'convert']
  159. else:
  160. exe = which('convert')
  161. if exe is None:
  162. raise OSError('Failed to find the ImageMagick convert executable, make sure it is present in PATH')
  163. cmd = [exe]
  164. if remove_alpha:
  165. cmd += ['-background', remove_alpha, '-alpha', 'remove']
  166. else:
  167. cmd += ['-background', 'none']
  168. if flip:
  169. cmd.append('-flip')
  170. if flop:
  171. cmd.append('-flop')
  172. cmd += ['--', path]
  173. if only_first_frame and has_multiple_frames:
  174. cmd[-1] += '[0]'
  175. cmd.append('-auto-orient')
  176. scaled = False
  177. width, height = m.width, m.height
  178. if scale_up:
  179. if width < available_width:
  180. r = available_width / width
  181. width, height = available_width, int(height * r)
  182. scaled = True
  183. if scaled or width > available_width or height > available_height:
  184. width, height = fit_image(width, height, available_width, available_height)
  185. resize_cmd = ['-resize', f'{width}x{height}!']
  186. if get_multiple_frames:
  187. # we have to coalesce, resize and de-coalesce all frames
  188. resize_cmd = ['-coalesce'] + resize_cmd + ['-deconstruct']
  189. cmd += resize_cmd
  190. cmd += ['-depth', '8', '-set', 'filename:f', '%w-%h-%g-%p']
  191. ans = RenderedImage(m.fmt, width, height, m.mode)
  192. if only_first_frame:
  193. ans.frames = [Frame(m.frames[0])]
  194. else:
  195. ans.frames = list(map(Frame, m.frames))
  196. bytes_per_pixel = 3 if m.mode == 'rgb' else 4
  197. def check_resize(frame: Frame) -> None:
  198. # ImageMagick sometimes generates RGBA images smaller than the specified
  199. # size. See https://github.com/kovidgoyal/kitty/issues/276 for examples
  200. sz = os.path.getsize(frame.path)
  201. expected_size = bytes_per_pixel * frame.width * frame.height
  202. if sz < expected_size:
  203. missing = expected_size - sz
  204. if missing % (bytes_per_pixel * width) != 0:
  205. raise ConvertFailed(
  206. path, 'ImageMagick failed to convert {} correctly,'
  207. ' it generated {} < {} of data (w={}, h={}, bpp={})'.format(
  208. path, sz, expected_size, frame.width, frame.height, bytes_per_pixel))
  209. frame.height -= missing // (bytes_per_pixel * frame.width)
  210. if frame.index == 0:
  211. ans.height = frame.height
  212. ans.width = frame.width
  213. with tempfile.TemporaryDirectory(dir=os.path.dirname(output_prefix)) as tdir:
  214. output_template = os.path.join(tdir, f'im-%[filename:f].{m.mode}')
  215. if get_multiple_frames:
  216. cmd.append('+adjoin')
  217. run_imagemagick(path, cmd + [output_template])
  218. unseen = {x.index for x in m}
  219. for x in os.listdir(tdir):
  220. try:
  221. parts = x.split('.', 1)[0].split('-')
  222. index = int(parts[-1])
  223. unseen.discard(index)
  224. f = ans.frames[index]
  225. f.width, f.height = map(positive_int, parts[1:3])
  226. sz, pos = parts[3].split('+', 1)
  227. f.canvas_width, f.canvas_height = map(positive_int, sz.split('x', 1))
  228. f.canvas_x, f.canvas_y = map(int, pos.split('+', 1))
  229. except Exception:
  230. raise OutdatedImageMagick(f'Unexpected output filename: {x!r} produced by ImageMagick command: {last_imagemagick_cmd}')
  231. f.path = output_prefix + f'-{index}.{m.mode}'
  232. os.rename(os.path.join(tdir, x), f.path)
  233. check_resize(f)
  234. f = ans.frames[0]
  235. if f.width != ans.width or f.height != ans.height:
  236. with open(f.path, 'r+b') as ff:
  237. data = ff.read()
  238. ff.seek(0)
  239. ff.truncate()
  240. cd = create_canvas(data, f.width, f.canvas_x, f.canvas_y, ans.width, ans.height, 3 if ans.mode == 'rgb' else 4)
  241. ff.write(cd)
  242. if get_multiple_frames:
  243. if unseen:
  244. raise ConvertFailed(path, f'Failed to render {len(unseen)} out of {len(m)} frames of animation')
  245. elif not ans.frames[0].path:
  246. raise ConvertFailed(path, 'Failed to render image')
  247. return ans
  248. def render_as_single_image(
  249. path: str, m: ImageData,
  250. available_width: int, available_height: int,
  251. scale_up: bool,
  252. tdir: str | None = None,
  253. remove_alpha: str = '', flip: bool = False, flop: bool = False,
  254. ) -> tuple[str, int, int]:
  255. import tempfile
  256. fd, output = tempfile.mkstemp(prefix='tty-graphics-protocol-', suffix=f'.{m.mode}', dir=tdir)
  257. os.close(fd)
  258. result = render_image(
  259. path, output, m, available_width, available_height, scale_up,
  260. only_first_frame=True, remove_alpha=remove_alpha, flip=flip, flop=flop)
  261. os.rename(result.frames[0].path, output)
  262. return output, result.width, result.height
  263. def can_display_images() -> bool:
  264. ans: bool | None = getattr(can_display_images, 'ans', None)
  265. if ans is None:
  266. ans = which('convert') is not None
  267. setattr(can_display_images, 'ans', ans)
  268. return ans
  269. ImageKey = tuple[str, int, int]
  270. SentImageKey = tuple[int, int, int]
  271. T = TypeVar('T')
  272. class Alias(Generic[T]):
  273. currently_processing: ClassVar[str] = ''
  274. def __init__(self, defval: T) -> None:
  275. self.name = ''
  276. self.defval = defval
  277. def __get__(self, instance: Optional['GraphicsCommand'], cls: type['GraphicsCommand'] | None = None) -> T:
  278. if instance is None:
  279. return self.defval
  280. return cast(T, instance._actual_values.get(self.name, self.defval))
  281. def __set__(self, instance: 'GraphicsCommand', val: T) -> None:
  282. if val == self.defval:
  283. instance._actual_values.pop(self.name, None)
  284. else:
  285. instance._actual_values[self.name] = val
  286. def __set_name__(self, owner: type['GraphicsCommand'], name: str) -> None:
  287. if len(name) == 1:
  288. Alias.currently_processing = name
  289. self.name = Alias.currently_processing
  290. class GraphicsCommand:
  291. a = action = Alias(cast(GRT_a, 't'))
  292. q = quiet = Alias(0)
  293. f = format = Alias(32)
  294. t = transmission_type = Alias(cast(GRT_t, 'd'))
  295. s = data_width = animation_state = Alias(0)
  296. v = data_height = loop_count = Alias(0)
  297. S = data_size = Alias(0)
  298. O = data_offset = Alias(0) # noqa
  299. i = image_id = Alias(0)
  300. I = image_number = Alias(0) # noqa
  301. p = placement_id = Alias(0)
  302. o = compression = Alias(cast(Optional[GRT_o], None))
  303. m = more = Alias(cast(GRT_m, 0))
  304. x = left_edge = Alias(0)
  305. y = top_edge = Alias(0)
  306. w = width = Alias(0)
  307. h = height = Alias(0)
  308. X = cell_x_offset = blend_mode = Alias(0)
  309. Y = cell_y_offset = bgcolor = Alias(0)
  310. c = columns = other_frame_number = dest_frame = Alias(0)
  311. r = rows = frame_number = source_frame = Alias(0)
  312. z = z_index = gap = Alias(0)
  313. C = cursor_movement = compose_mode = Alias(cast(GRT_C, 0))
  314. d = delete_action = Alias(cast(GRT_d, 'a'))
  315. def __init__(self) -> None:
  316. self._actual_values: dict[str, Any] = {}
  317. def __repr__(self) -> str:
  318. return self.serialize().decode('ascii').replace('\033', '^]')
  319. def clone(self) -> 'GraphicsCommand':
  320. ans = GraphicsCommand()
  321. ans._actual_values = self._actual_values.copy()
  322. return ans
  323. def serialize(self, payload: bytes | str = b'') -> bytes:
  324. items = []
  325. for k, val in self._actual_values.items():
  326. items.append(f'{k}={val}')
  327. ans: list[bytes] = []
  328. w = ans.append
  329. w(b'\033_G')
  330. w(','.join(items).encode('ascii'))
  331. if payload:
  332. w(b';')
  333. if isinstance(payload, str):
  334. payload = standard_b64encode(payload.encode('utf-8'))
  335. w(payload)
  336. w(b'\033\\')
  337. return b''.join(ans)
  338. def clear(self) -> None:
  339. self._actual_values = {}
  340. def iter_transmission_chunks(self, data: bytes | None = None, level: int = -1, compression_threshold: int = 1024) -> Iterator[bytes]:
  341. if data is None:
  342. yield self.serialize()
  343. return
  344. gc = self.clone()
  345. gc.S = len(data)
  346. if level and len(data) >= compression_threshold:
  347. import zlib
  348. compressed = zlib.compress(data, level)
  349. if len(compressed) < len(data):
  350. gc.o = 'z'
  351. data = compressed
  352. gc.S = len(data)
  353. data = standard_b64encode(data)
  354. while data:
  355. chunk, data = data[:4096], data[4096:]
  356. gc.m = 1 if data else 0
  357. yield gc.serialize(chunk)
  358. gc.clear()
  359. class Placement:
  360. cmd: GraphicsCommand
  361. x: int = 0
  362. y: int = 0
  363. def __init__(self, cmd: GraphicsCommand, x: int = 0, y: int = 0):
  364. self.cmd = cmd
  365. self.x = x
  366. self.y = y
  367. class ImageManager:
  368. def __init__(self, handler: HandlerType):
  369. self.image_id_counter = count()
  370. self.handler = handler
  371. self.filesystem_ok: bool | None = None
  372. self.image_data: dict[str, ImageData] = {}
  373. self.failed_images: dict[str, Exception] = {}
  374. self.converted_images: dict[ImageKey, ImageKey] = {}
  375. self.sent_images: dict[ImageKey, int] = {}
  376. self.image_id_to_image_data: dict[int, ImageData] = {}
  377. self.image_id_to_converted_data: dict[int, ImageKey] = {}
  378. self.transmission_status: dict[int, str | int] = {}
  379. self.placements_in_flight: DefaultDict[int, Deque[Placement]] = defaultdict(deque)
  380. self.update_image_placement_for_resend: Callable[[int, Placement], bool] | None
  381. @property
  382. def next_image_id(self) -> int:
  383. return next(self.image_id_counter) + 2
  384. @property
  385. def screen_size(self) -> ScreenSize:
  386. return self.handler.screen_size
  387. def __enter__(self) -> None:
  388. import tempfile
  389. self.tdir = tempfile.mkdtemp(prefix='kitten-images-')
  390. with tempfile.NamedTemporaryFile(dir=self.tdir, delete=False) as f:
  391. f.write(b'abcd')
  392. gc = GraphicsCommand()
  393. gc.a = 'q'
  394. gc.s = gc.v = gc.i = 1
  395. gc.t = 'f'
  396. self.handler.cmd.gr_command(gc, standard_b64encode(f.name.encode(fsenc)))
  397. def __exit__(self, *a: Any) -> None:
  398. import shutil
  399. shutil.rmtree(self.tdir, ignore_errors=True)
  400. self.handler.cmd.clear_images_on_screen(delete_data=True)
  401. self.delete_all_sent_images()
  402. del self.handler
  403. def delete_all_sent_images(self) -> None:
  404. gc = GraphicsCommand()
  405. gc.a = 'd'
  406. for img_id in self.transmission_status:
  407. gc.i = img_id
  408. self.handler.cmd.gr_command(gc)
  409. self.transmission_status.clear()
  410. def handle_response(self, apc: str) -> None:
  411. cdata, payload = apc[1:].partition(';')[::2]
  412. control = {}
  413. for x in cdata.split(','):
  414. k, v = x.partition('=')[::2]
  415. control[k] = v
  416. try:
  417. image_id = int(control.get('i', '0'))
  418. except Exception:
  419. image_id = 0
  420. if image_id == 1:
  421. self.filesystem_ok = payload == 'OK'
  422. return
  423. if not image_id:
  424. return
  425. if not self.transmission_status.get(image_id):
  426. self.transmission_status[image_id] = payload
  427. else:
  428. in_flight = self.placements_in_flight[image_id]
  429. if in_flight:
  430. pl = in_flight.popleft()
  431. if payload.startswith('ENOENT:'):
  432. with suppress(Exception):
  433. self.resend_image(image_id, pl)
  434. if not in_flight:
  435. self.placements_in_flight.pop(image_id, None)
  436. def resend_image(self, image_id: int, pl: Placement) -> None:
  437. if self.update_image_placement_for_resend is not None and not self.update_image_placement_for_resend(image_id, pl):
  438. return
  439. image_data = self.image_id_to_image_data[image_id]
  440. skey = self.image_id_to_converted_data[image_id]
  441. self.transmit_image(image_data, image_id, *skey)
  442. with cursor(self.handler.write):
  443. self.handler.cmd.set_cursor_position(pl.x, pl.y)
  444. self.handler.cmd.gr_command(pl.cmd)
  445. def send_image(self, path: str, max_cols: int | None = None, max_rows: int | None = None, scale_up: bool = False) -> SentImageKey:
  446. path = os.path.abspath(path)
  447. if path in self.failed_images:
  448. raise self.failed_images[path]
  449. if path not in self.image_data:
  450. try:
  451. self.image_data[path] = identify(path)
  452. except Exception as e:
  453. self.failed_images[path] = e
  454. raise
  455. m = self.image_data[path]
  456. ss = self.screen_size
  457. if max_cols is None:
  458. max_cols = ss.cols
  459. if max_rows is None:
  460. max_rows = ss.rows
  461. available_width = max_cols * ss.cell_width
  462. available_height = max_rows * ss.cell_height
  463. key = path, available_width, available_height
  464. skey = self.converted_images.get(key)
  465. if skey is None:
  466. try:
  467. self.converted_images[key] = skey = self.convert_image(path, available_width, available_height, m, scale_up)
  468. except Exception as e:
  469. self.failed_images[path] = e
  470. raise
  471. final_width, final_height = skey[1:]
  472. if final_width == 0:
  473. return 0, 0, 0
  474. image_id = self.sent_images.get(skey)
  475. if image_id is None:
  476. image_id = self.next_image_id
  477. self.transmit_image(m, image_id, *skey)
  478. self.sent_images[skey] = image_id
  479. self.image_id_to_converted_data[image_id] = skey
  480. self.image_id_to_image_data[image_id] = m
  481. return image_id, skey[1], skey[2]
  482. def hide_image(self, image_id: int) -> None:
  483. gc = GraphicsCommand()
  484. gc.a = 'd'
  485. gc.i = image_id
  486. self.handler.cmd.gr_command(gc)
  487. def show_image(self, image_id: int, x: int, y: int, src_rect: tuple[int, int, int, int] | None = None) -> None:
  488. gc = GraphicsCommand()
  489. gc.a = 'p'
  490. gc.i = image_id
  491. if src_rect is not None:
  492. gc.x, gc.y, gc.w, gc.h = map(int, src_rect)
  493. self.placements_in_flight[image_id].append(Placement(gc, x, y))
  494. with cursor(self.handler.write):
  495. self.handler.cmd.set_cursor_position(x, y)
  496. self.handler.cmd.gr_command(gc)
  497. def convert_image(self, path: str, available_width: int, available_height: int, image_data: ImageData, scale_up: bool = False) -> ImageKey:
  498. rgba_path, width, height = render_as_single_image(path, image_data, available_width, available_height, scale_up, tdir=self.tdir)
  499. return rgba_path, width, height
  500. def transmit_image(self, image_data: ImageData, image_id: int, rgba_path: str, width: int, height: int) -> int:
  501. self.transmission_status[image_id] = 0
  502. gc = GraphicsCommand()
  503. gc.a = 't'
  504. gc.f = image_data.transmit_fmt
  505. gc.s = width
  506. gc.v = height
  507. gc.i = image_id
  508. if self.filesystem_ok:
  509. gc.t = 'f'
  510. self.handler.cmd.gr_command(
  511. gc, standard_b64encode(rgba_path.encode(fsenc)))
  512. else:
  513. import zlib
  514. with open(rgba_path, 'rb') as f:
  515. data = f.read()
  516. gc.S = len(data)
  517. data = zlib.compress(data)
  518. gc.o = 'z'
  519. data = standard_b64encode(data)
  520. while data:
  521. chunk, data = data[:4096], data[4096:]
  522. gc.m = 1 if data else 0
  523. self.handler.cmd.gr_command(gc, chunk)
  524. gc.clear()
  525. return image_id