transmit.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413
  1. // License: GPLv3 Copyright: 2022, Kovid Goyal, <kovid at kovidgoyal.net>
  2. package icat
  3. import (
  4. "bytes"
  5. "crypto/rand"
  6. "encoding/binary"
  7. "errors"
  8. "fmt"
  9. "io"
  10. "kitty"
  11. "math"
  12. not_rand "math/rand/v2"
  13. "os"
  14. "path/filepath"
  15. "strings"
  16. "kitty/tools/tui"
  17. "kitty/tools/tui/graphics"
  18. "kitty/tools/tui/loop"
  19. "kitty/tools/utils"
  20. "kitty/tools/utils/images"
  21. "kitty/tools/utils/shm"
  22. )
  23. var _ = fmt.Print
  24. type passthrough_type int
  25. const (
  26. no_passthrough passthrough_type = iota
  27. tmux_passthrough
  28. )
  29. func new_graphics_command(imgd *image_data) *graphics.GraphicsCommand {
  30. gc := graphics.GraphicsCommand{}
  31. switch imgd.passthrough_mode {
  32. case tmux_passthrough:
  33. gc.WrapPrefix = "\033Ptmux;"
  34. gc.WrapSuffix = "\033\\"
  35. gc.EncodeSerializedDataFunc = func(x string) string { return strings.ReplaceAll(x, "\033", "\033\033") }
  36. }
  37. return &gc
  38. }
  39. func gc_for_image(imgd *image_data, frame_num int, frame *image_frame) *graphics.GraphicsCommand {
  40. gc := new_graphics_command(imgd)
  41. gc.SetDataWidth(uint64(frame.width)).SetDataHeight(uint64(frame.height))
  42. gc.SetQuiet(graphics.GRT_quiet_silent)
  43. gc.SetFormat(frame.transmission_format)
  44. if imgd.image_number != 0 {
  45. gc.SetImageNumber(imgd.image_number)
  46. }
  47. if imgd.image_id != 0 {
  48. gc.SetImageId(imgd.image_id)
  49. }
  50. if frame_num == 0 {
  51. gc.SetAction(graphics.GRT_action_transmit_and_display)
  52. if imgd.use_unicode_placeholder {
  53. gc.SetUnicodePlaceholder(graphics.GRT_create_unicode_placeholder)
  54. gc.SetColumns(uint64(imgd.width_cells))
  55. gc.SetRows(uint64(imgd.height_cells))
  56. }
  57. if imgd.cell_x_offset > 0 {
  58. gc.SetXOffset(uint64(imgd.cell_x_offset))
  59. }
  60. if z_index != 0 {
  61. gc.SetZIndex(z_index)
  62. }
  63. if place != nil {
  64. gc.SetCursorMovement(graphics.GRT_cursor_static)
  65. }
  66. } else {
  67. gc.SetAction(graphics.GRT_action_frame)
  68. gc.SetGap(int32(frame.delay_ms))
  69. if frame.compose_onto > 0 {
  70. gc.SetOverlaidFrame(uint64(frame.compose_onto))
  71. } else {
  72. bg := (uint32(frame.disposal_background.R) << 24) | (uint32(frame.disposal_background.G) << 16) | (uint32(frame.disposal_background.B) << 8) | uint32(frame.disposal_background.A)
  73. gc.SetBackgroundColor(bg)
  74. }
  75. gc.SetLeftEdge(uint64(frame.left)).SetTopEdge(uint64(frame.top))
  76. }
  77. return gc
  78. }
  79. func transmit_shm(imgd *image_data, frame_num int, frame *image_frame) (err error) {
  80. var mmap shm.MMap
  81. var data_size int64
  82. if frame.in_memory_bytes == nil {
  83. f, err := os.Open(frame.filename)
  84. if err != nil {
  85. return fmt.Errorf("Failed to open image data output file: %s with error: %w", frame.filename, err)
  86. }
  87. defer f.Close()
  88. data_size, _ = f.Seek(0, io.SeekEnd)
  89. _, _ = f.Seek(0, io.SeekStart)
  90. mmap, err = shm.CreateTemp("icat-*", uint64(data_size))
  91. if err != nil {
  92. return fmt.Errorf("Failed to create a SHM file for transmission: %w", err)
  93. }
  94. dest := mmap.Slice()
  95. for len(dest) > 0 {
  96. n, err := f.Read(dest)
  97. dest = dest[n:]
  98. if err != nil {
  99. if errors.Is(err, io.EOF) {
  100. break
  101. }
  102. _ = mmap.Unlink()
  103. return fmt.Errorf("Failed to read data from image output data file: %w", err)
  104. }
  105. }
  106. } else {
  107. if frame.shm == nil {
  108. data_size = int64(len(frame.in_memory_bytes))
  109. mmap, err = shm.CreateTemp("icat-*", uint64(data_size))
  110. if err != nil {
  111. return fmt.Errorf("Failed to create a SHM file for transmission: %w", err)
  112. }
  113. copy(mmap.Slice(), frame.in_memory_bytes)
  114. } else {
  115. mmap = frame.shm
  116. frame.shm = nil
  117. }
  118. }
  119. gc := gc_for_image(imgd, frame_num, frame)
  120. gc.SetTransmission(graphics.GRT_transmission_sharedmem)
  121. gc.SetDataSize(uint64(data_size))
  122. err = gc.WriteWithPayloadTo(os.Stdout, utils.UnsafeStringToBytes(mmap.Name()))
  123. mmap.Close()
  124. return
  125. }
  126. func transmit_file(imgd *image_data, frame_num int, frame *image_frame) (err error) {
  127. is_temp := false
  128. fname := ""
  129. var data_size int
  130. if frame.in_memory_bytes == nil {
  131. is_temp = frame.filename_is_temporary
  132. fname, err = filepath.Abs(frame.filename)
  133. if err != nil {
  134. return fmt.Errorf("Failed to convert image data output file: %s to absolute path with error: %w", frame.filename, err)
  135. }
  136. frame.filename = "" // so it isn't deleted in cleanup
  137. } else {
  138. is_temp = true
  139. if frame.shm != nil && frame.shm.FileSystemName() != "" {
  140. fname = frame.shm.FileSystemName()
  141. frame.shm.Close()
  142. frame.shm = nil
  143. } else {
  144. f, err := images.CreateTempInRAM()
  145. if err != nil {
  146. return fmt.Errorf("Failed to create a temp file for image data transmission: %w", err)
  147. }
  148. data_size = len(frame.in_memory_bytes)
  149. _, err = bytes.NewBuffer(frame.in_memory_bytes).WriteTo(f)
  150. f.Close()
  151. if err != nil {
  152. return fmt.Errorf("Failed to write image data to temp file for transmission: %w", err)
  153. }
  154. fname = f.Name()
  155. }
  156. }
  157. gc := gc_for_image(imgd, frame_num, frame)
  158. if is_temp {
  159. gc.SetTransmission(graphics.GRT_transmission_tempfile)
  160. } else {
  161. gc.SetTransmission(graphics.GRT_transmission_file)
  162. }
  163. if data_size > 0 {
  164. gc.SetDataSize(uint64(data_size))
  165. }
  166. return gc.WriteWithPayloadTo(os.Stdout, utils.UnsafeStringToBytes(fname))
  167. }
  168. func transmit_stream(imgd *image_data, frame_num int, frame *image_frame) (err error) {
  169. data := frame.in_memory_bytes
  170. if data == nil {
  171. f, err := os.Open(frame.filename)
  172. if err != nil {
  173. return fmt.Errorf("Failed to open image data output file: %s with error: %w", frame.filename, err)
  174. }
  175. data, err = io.ReadAll(f)
  176. f.Close()
  177. if err != nil {
  178. return fmt.Errorf("Failed to read data from image output data file: %w", err)
  179. }
  180. }
  181. gc := gc_for_image(imgd, frame_num, frame)
  182. return gc.WriteWithPayloadTo(os.Stdout, data)
  183. }
  184. func calculate_in_cell_x_offset(width, cell_width int) int {
  185. extra_pixels := width % cell_width
  186. if extra_pixels == 0 {
  187. return 0
  188. }
  189. switch opts.Align {
  190. case "left":
  191. return 0
  192. case "right":
  193. return cell_width - extra_pixels
  194. default:
  195. return (cell_width - extra_pixels) / 2
  196. }
  197. }
  198. func place_cursor(imgd *image_data) {
  199. cw := max(int(screen_size.Xpixel)/int(screen_size.Col), 1)
  200. ch := max(int(screen_size.Ypixel)/int(screen_size.Row), 1)
  201. imgd.cell_x_offset = calculate_in_cell_x_offset(imgd.canvas_width, cw)
  202. imgd.width_cells = int(math.Ceil(float64(imgd.canvas_width) / float64(cw)))
  203. imgd.height_cells = int(math.Ceil(float64(imgd.canvas_height) / float64(ch)))
  204. if place == nil {
  205. switch opts.Align {
  206. case "center":
  207. imgd.move_x_by = (int(screen_size.Col) - imgd.width_cells) / 2
  208. case "right":
  209. imgd.move_x_by = (int(screen_size.Col) - imgd.width_cells)
  210. }
  211. } else {
  212. imgd.move_to.x = place.left + 1
  213. imgd.move_to.y = place.top + 1
  214. switch opts.Align {
  215. case "center":
  216. imgd.move_to.x += (place.width - imgd.width_cells) / 2
  217. case "right":
  218. imgd.move_to.x += (place.width - imgd.width_cells)
  219. }
  220. }
  221. }
  222. func next_random() (ans uint32) {
  223. for ans == 0 {
  224. b := make([]byte, 4)
  225. _, err := rand.Read(b)
  226. if err == nil {
  227. ans = binary.LittleEndian.Uint32(b[:])
  228. } else {
  229. ans = not_rand.Uint32()
  230. }
  231. }
  232. return ans
  233. }
  234. func write_unicode_placeholder(imgd *image_data) {
  235. prefix := ""
  236. foreground := fmt.Sprintf("\033[38:2:%d:%d:%dm", (imgd.image_id>>16)&255, (imgd.image_id>>8)&255, imgd.image_id&255)
  237. os.Stdout.WriteString(foreground)
  238. restore := "\033[39m"
  239. if imgd.move_to.y > 0 {
  240. os.Stdout.WriteString(loop.SAVE_CURSOR)
  241. restore += loop.RESTORE_CURSOR
  242. } else if imgd.move_x_by > 0 {
  243. prefix = strings.Repeat(" ", imgd.move_x_by)
  244. }
  245. defer func() { os.Stdout.WriteString(restore) }()
  246. if imgd.move_to.y > 0 {
  247. fmt.Printf(loop.MoveCursorToTemplate, imgd.move_to.y, 0)
  248. }
  249. id_char := string(images.NumberToDiacritic[(imgd.image_id>>24)&255])
  250. for r := 0; r < imgd.height_cells; r++ {
  251. if imgd.move_to.x > 0 {
  252. fmt.Printf("\x1b[%dC", imgd.move_to.x-1)
  253. } else {
  254. os.Stdout.WriteString(prefix)
  255. }
  256. for c := 0; c < imgd.width_cells; c++ {
  257. os.Stdout.WriteString(string(kitty.ImagePlaceholderChar) + string(images.NumberToDiacritic[r]) + string(images.NumberToDiacritic[c]) + id_char)
  258. }
  259. os.Stdout.WriteString("\n\r")
  260. }
  261. }
  262. var seen_image_ids *utils.Set[uint32]
  263. func transmit_image(imgd *image_data) {
  264. if seen_image_ids == nil {
  265. seen_image_ids = utils.NewSet[uint32](32)
  266. }
  267. defer func() {
  268. for _, frame := range imgd.frames {
  269. if frame.filename_is_temporary && frame.filename != "" {
  270. os.Remove(frame.filename)
  271. frame.filename = ""
  272. }
  273. if frame.shm != nil {
  274. _ = frame.shm.Unlink()
  275. frame.shm.Close()
  276. frame.shm = nil
  277. }
  278. frame.in_memory_bytes = nil
  279. }
  280. }()
  281. var f func(*image_data, int, *image_frame) error
  282. if opts.TransferMode != "detect" {
  283. switch opts.TransferMode {
  284. case "file":
  285. f = transmit_file
  286. case "memory":
  287. f = transmit_shm
  288. case "stream":
  289. f = transmit_stream
  290. }
  291. }
  292. if f == nil && transfer_by_memory == supported && imgd.frames[0].in_memory_bytes != nil {
  293. f = transmit_shm
  294. }
  295. if f == nil && transfer_by_file == supported {
  296. f = transmit_file
  297. }
  298. if f == nil {
  299. f = transmit_stream
  300. }
  301. if imgd.image_id == 0 {
  302. if imgd.use_unicode_placeholder {
  303. for imgd.image_id&0xFF000000 == 0 || imgd.image_id&0x00FFFF00 == 0 || seen_image_ids.Has(imgd.image_id) {
  304. // Generate a 32-bit image id using rejection sampling such that the most
  305. // significant byte and the two bytes in the middle are non-zero to avoid
  306. // collisions with applications that cannot represent non-zero most
  307. // significant bytes (which is represented by the third combining character)
  308. // or two non-zero bytes in the middle (which requires 24-bit color mode).
  309. imgd.image_id = next_random()
  310. }
  311. seen_image_ids.Add(imgd.image_id)
  312. } else {
  313. if len(imgd.frames) > 1 {
  314. for imgd.image_number == 0 {
  315. imgd.image_number = next_random()
  316. }
  317. }
  318. }
  319. }
  320. place_cursor(imgd)
  321. if imgd.use_unicode_placeholder && utils.Max(imgd.width_cells, imgd.height_cells) >= len(images.NumberToDiacritic) {
  322. imgd.err = fmt.Errorf("Image too large to be displayed using Unicode placeholders. Maximum size is %dx%d cells", len(images.NumberToDiacritic), len(images.NumberToDiacritic))
  323. return
  324. }
  325. switch imgd.passthrough_mode {
  326. case tmux_passthrough:
  327. imgd.err = tui.TmuxAllowPassthrough()
  328. if imgd.err != nil {
  329. return
  330. }
  331. }
  332. fmt.Print("\r")
  333. if !imgd.use_unicode_placeholder {
  334. if imgd.move_x_by > 0 {
  335. fmt.Printf("\x1b[%dC", imgd.move_x_by)
  336. }
  337. if imgd.move_to.x > 0 {
  338. fmt.Printf(loop.MoveCursorToTemplate, imgd.move_to.y, imgd.move_to.x)
  339. }
  340. }
  341. frame_control_cmd := new_graphics_command(imgd)
  342. frame_control_cmd.SetAction(graphics.GRT_action_animate)
  343. if imgd.image_id != 0 {
  344. frame_control_cmd.SetImageId(imgd.image_id)
  345. } else {
  346. frame_control_cmd.SetImageNumber(imgd.image_number)
  347. }
  348. is_animated := len(imgd.frames) > 1
  349. for frame_num, frame := range imgd.frames {
  350. err := f(imgd, frame_num, frame)
  351. if err != nil {
  352. imgd.err = err
  353. return
  354. }
  355. if is_animated {
  356. switch frame_num {
  357. case 0:
  358. // set gap for the first frame and number of loops for the animation
  359. c := frame_control_cmd
  360. c.SetTargetFrame(uint64(frame.number))
  361. c.SetGap(int32(frame.delay_ms))
  362. switch {
  363. case opts.Loop < 0:
  364. c.SetNumberOfLoops(1)
  365. case opts.Loop > 0:
  366. c.SetNumberOfLoops(uint64(opts.Loop) + 1)
  367. }
  368. if imgd.err = c.WriteWithPayloadTo(os.Stdout, nil); imgd.err != nil {
  369. return
  370. }
  371. case 1:
  372. c := frame_control_cmd
  373. c.SetAnimationControl(2) // set animation to loading mode
  374. if imgd.err = c.WriteWithPayloadTo(os.Stdout, nil); imgd.err != nil {
  375. return
  376. }
  377. }
  378. }
  379. }
  380. if imgd.use_unicode_placeholder {
  381. write_unicode_placeholder(imgd)
  382. }
  383. if is_animated {
  384. c := frame_control_cmd
  385. c.SetAnimationControl(3) // set animation to normal mode
  386. if imgd.err = c.WriteWithPayloadTo(os.Stdout, nil); imgd.err != nil {
  387. return
  388. }
  389. }
  390. if imgd.move_to.x == 0 {
  391. fmt.Println() // ensure cursor is on new line
  392. }
  393. }