loading.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630
  1. // License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
  2. package images
  3. import (
  4. "bytes"
  5. "encoding/json"
  6. "errors"
  7. "fmt"
  8. "image"
  9. "image/color"
  10. "image/gif"
  11. "io"
  12. "os"
  13. "os/exec"
  14. "path/filepath"
  15. "strconv"
  16. "strings"
  17. "sync"
  18. "kitty/tools/utils"
  19. "kitty/tools/utils/shm"
  20. "github.com/edwvee/exiffix"
  21. "github.com/kovidgoyal/imaging"
  22. "golang.org/x/exp/slices"
  23. )
  24. var _ = fmt.Print
  25. const TempTemplate = "kitty-tty-graphics-protocol-*"
  26. func CreateTemp() (*os.File, error) {
  27. return os.CreateTemp("", TempTemplate)
  28. }
  29. func CreateTempInRAM() (*os.File, error) {
  30. if shm.SHM_DIR != "" {
  31. f, err := os.CreateTemp(shm.SHM_DIR, TempTemplate)
  32. if err == nil {
  33. return f, err
  34. }
  35. }
  36. return CreateTemp()
  37. }
  38. type ImageFrame struct {
  39. Width, Height, Left, Top int
  40. Number int // 1-based number
  41. Compose_onto int // number of frame to compose onto
  42. Delay_ms int32 // negative for gapless frame, zero ignored, positive is number of ms
  43. Is_opaque bool
  44. Img image.Image
  45. }
  46. func (self *ImageFrame) DataAsSHM(pattern string) (ans shm.MMap, err error) {
  47. bytes_per_pixel := 4
  48. if self.Is_opaque {
  49. bytes_per_pixel = 3
  50. }
  51. ans, err = shm.CreateTemp(pattern, uint64(self.Width*self.Height*bytes_per_pixel))
  52. if err != nil {
  53. return nil, err
  54. }
  55. switch img := self.Img.(type) {
  56. case *NRGB:
  57. if bytes_per_pixel == 3 {
  58. copy(ans.Slice(), img.Pix)
  59. return
  60. }
  61. case *image.NRGBA:
  62. if bytes_per_pixel == 4 {
  63. copy(ans.Slice(), img.Pix)
  64. return
  65. }
  66. }
  67. dest_rect := image.Rect(0, 0, self.Width, self.Height)
  68. var final_img image.Image
  69. switch bytes_per_pixel {
  70. case 3:
  71. rgb := &NRGB{Pix: ans.Slice(), Stride: bytes_per_pixel * self.Width, Rect: dest_rect}
  72. final_img = rgb
  73. case 4:
  74. rgba := &image.NRGBA{Pix: ans.Slice(), Stride: bytes_per_pixel * self.Width, Rect: dest_rect}
  75. final_img = rgba
  76. }
  77. ctx := Context{}
  78. ctx.PasteCenter(final_img, self.Img, nil)
  79. return
  80. }
  81. func (self *ImageFrame) Data() (ans []byte) {
  82. bytes_per_pixel := 4
  83. if self.Is_opaque {
  84. bytes_per_pixel = 3
  85. }
  86. switch img := self.Img.(type) {
  87. case *NRGB:
  88. if bytes_per_pixel == 3 {
  89. return img.Pix
  90. }
  91. case *image.NRGBA:
  92. if bytes_per_pixel == 4 {
  93. return img.Pix
  94. }
  95. }
  96. dest_rect := image.Rect(0, 0, self.Width, self.Height)
  97. var final_img image.Image
  98. switch bytes_per_pixel {
  99. case 3:
  100. rgb := NewNRGB(dest_rect)
  101. final_img = rgb
  102. ans = rgb.Pix
  103. case 4:
  104. rgba := image.NewNRGBA(dest_rect)
  105. final_img = rgba
  106. ans = rgba.Pix
  107. }
  108. ctx := Context{}
  109. ctx.PasteCenter(final_img, self.Img, nil)
  110. return
  111. }
  112. type ImageData struct {
  113. Width, Height int
  114. Format_uppercase string
  115. Frames []*ImageFrame
  116. }
  117. func (self *ImageFrame) Resize(x_frac, y_frac float64) *ImageFrame {
  118. b := self.Img.Bounds()
  119. left, top, width, height := b.Min.X, b.Min.Y, b.Dx(), b.Dy()
  120. ans := *self
  121. ans.Width = int(x_frac * float64(width))
  122. ans.Height = int(y_frac * float64(height))
  123. ans.Img = imaging.Resize(self.Img, ans.Width, ans.Height, imaging.Lanczos)
  124. ans.Left = int(x_frac * float64(left))
  125. ans.Top = int(y_frac * float64(top))
  126. return &ans
  127. }
  128. func (self *ImageData) Resize(x_frac, y_frac float64) *ImageData {
  129. ans := *self
  130. ans.Frames = utils.Map(func(f *ImageFrame) *ImageFrame { return f.Resize(x_frac, y_frac) }, self.Frames)
  131. if len(ans.Frames) > 0 {
  132. ans.Width, ans.Height = ans.Frames[0].Width, ans.Frames[0].Height
  133. }
  134. return &ans
  135. }
  136. func CalcMinimumGIFGap(gaps []int) int {
  137. // Some broken GIF images have all zero gaps, browsers with their usual
  138. // idiot ideas render these with a default 100ms gap https://bugzilla.mozilla.org/show_bug.cgi?id=125137
  139. // Browsers actually force a 100ms gap at any zero gap frame, but that
  140. // just means it is impossible to deliberately use zero gap frames for
  141. // sophisticated blending, so we dont do that.
  142. max_gap := utils.Max(0, gaps...)
  143. min_gap := 0
  144. if max_gap <= 0 {
  145. min_gap = 10
  146. }
  147. return min_gap
  148. }
  149. func SetGIFFrameDisposal(number, anchor_frame int, disposal byte) (int, int) {
  150. compose_onto := 0
  151. if number > 1 {
  152. switch disposal {
  153. case gif.DisposalNone:
  154. compose_onto = number - 1
  155. anchor_frame = number
  156. case gif.DisposalBackground:
  157. // see https://github.com/golang/go/issues/20694
  158. anchor_frame = number
  159. case gif.DisposalPrevious:
  160. compose_onto = anchor_frame
  161. }
  162. }
  163. return anchor_frame, compose_onto
  164. }
  165. func MakeTempDir(template string) (ans string, err error) {
  166. if template == "" {
  167. template = "kitty-img-*"
  168. }
  169. if shm.SHM_DIR != "" {
  170. ans, err = os.MkdirTemp(shm.SHM_DIR, template)
  171. if err == nil {
  172. return
  173. }
  174. }
  175. return os.MkdirTemp("", template)
  176. }
  177. func check_resize(frame *ImageFrame, filename string) error {
  178. // ImageMagick sometimes generates RGBA images smaller than the specified
  179. // size. See https://github.com/kovidgoyal/kitty/issues/276 for examples
  180. s, err := os.Stat(filename)
  181. if err != nil {
  182. return err
  183. }
  184. sz := int(s.Size())
  185. bytes_per_pixel := 4
  186. if frame.Is_opaque {
  187. bytes_per_pixel = 3
  188. }
  189. expected_size := bytes_per_pixel * frame.Width * frame.Height
  190. if sz < expected_size {
  191. missing := expected_size - sz
  192. if missing%(bytes_per_pixel*frame.Width) != 0 {
  193. return fmt.Errorf("ImageMagick failed to resize correctly. It generated %d < %d of data (w=%d h=%d bpp=%d)", sz, expected_size, frame.Width, frame.Height, bytes_per_pixel)
  194. }
  195. frame.Height -= missing / (bytes_per_pixel * frame.Width)
  196. }
  197. return nil
  198. }
  199. func (frame *ImageFrame) set_delay(min_gap, delay int) {
  200. frame.Delay_ms = int32(max(min_gap, delay) * 10)
  201. if frame.Delay_ms == 0 {
  202. frame.Delay_ms = -1 // gapless frame in the graphics protocol
  203. }
  204. }
  205. func open_native_gif(f io.Reader, ans *ImageData) error {
  206. gif_frames, err := gif.DecodeAll(f)
  207. if err != nil {
  208. return err
  209. }
  210. min_gap := CalcMinimumGIFGap(gif_frames.Delay)
  211. anchor_frame := 1
  212. for i, paletted_img := range gif_frames.Image {
  213. b := paletted_img.Bounds()
  214. frame := ImageFrame{Img: paletted_img, Left: b.Min.X, Top: b.Min.Y, Width: b.Dx(), Height: b.Dy(), Number: len(ans.Frames) + 1, Is_opaque: paletted_img.Opaque()}
  215. frame.set_delay(min_gap, gif_frames.Delay[i])
  216. anchor_frame, frame.Compose_onto = SetGIFFrameDisposal(frame.Number, anchor_frame, gif_frames.Disposal[i])
  217. ans.Frames = append(ans.Frames, &frame)
  218. }
  219. return nil
  220. }
  221. func OpenNativeImageFromReader(f io.ReadSeeker) (ans *ImageData, err error) {
  222. c, fmt, err := image.DecodeConfig(f)
  223. if err != nil {
  224. return nil, err
  225. }
  226. _, _ = f.Seek(0, io.SeekStart)
  227. ans = &ImageData{Width: c.Width, Height: c.Height, Format_uppercase: strings.ToUpper(fmt)}
  228. if ans.Format_uppercase == "GIF" {
  229. err = open_native_gif(f, ans)
  230. if err != nil {
  231. return nil, err
  232. }
  233. } else {
  234. img, _, err := exiffix.Decode(f)
  235. if err != nil {
  236. return nil, err
  237. }
  238. b := img.Bounds()
  239. ans.Frames = []*ImageFrame{{Img: img, Left: b.Min.X, Top: b.Min.Y, Width: b.Dx(), Height: b.Dy()}}
  240. ans.Frames[0].Is_opaque = c.ColorModel == color.YCbCrModel || c.ColorModel == color.GrayModel || c.ColorModel == color.Gray16Model || c.ColorModel == color.CMYKModel || ans.Format_uppercase == "JPEG" || ans.Format_uppercase == "JPG" || IsOpaque(img)
  241. }
  242. return
  243. }
  244. var MagickExe = sync.OnceValue(func() string {
  245. return utils.FindExe("magick")
  246. })
  247. func RunMagick(path string, cmd []string) ([]byte, error) {
  248. if MagickExe() != "magick" {
  249. cmd = append([]string{MagickExe()}, cmd...)
  250. }
  251. c := exec.Command(cmd[0], cmd[1:]...)
  252. output, err := c.Output()
  253. if err != nil {
  254. var exit_err *exec.ExitError
  255. if errors.As(err, &exit_err) {
  256. return nil, fmt.Errorf("Running the command: %s\nFailed with error:\n%s", strings.Join(cmd, " "), string(exit_err.Stderr))
  257. }
  258. return nil, fmt.Errorf("Could not find the program: %#v. Is ImageMagick installed and in your PATH?", cmd[0])
  259. }
  260. return output, nil
  261. }
  262. type IdentifyOutput struct {
  263. Fmt, Canvas, Transparency, Gap, Index, Size, Dpi, Dispose, Orientation string
  264. }
  265. type IdentifyRecord struct {
  266. Fmt_uppercase string
  267. Gap int
  268. Canvas struct{ Width, Height, Left, Top int }
  269. Width, Height int
  270. Dpi struct{ X, Y float64 }
  271. Index int
  272. Is_opaque bool
  273. Needs_blend bool
  274. Disposal int
  275. Dimensions_swapped bool
  276. }
  277. func parse_identify_record(ans *IdentifyRecord, raw *IdentifyOutput) (err error) {
  278. ans.Fmt_uppercase = strings.ToUpper(raw.Fmt)
  279. if raw.Gap != "" {
  280. ans.Gap, err = strconv.Atoi(raw.Gap)
  281. if err != nil {
  282. return fmt.Errorf("Invalid gap value in identify output: %s", raw.Gap)
  283. }
  284. ans.Gap = max(0, ans.Gap)
  285. }
  286. area, pos, found := strings.Cut(raw.Canvas, "+")
  287. ok := false
  288. if found {
  289. w, h, found := strings.Cut(area, "x")
  290. if found {
  291. ans.Canvas.Width, err = strconv.Atoi(w)
  292. if err == nil {
  293. ans.Canvas.Height, err = strconv.Atoi(h)
  294. if err == nil {
  295. x, y, found := strings.Cut(pos, "+")
  296. if found {
  297. ans.Canvas.Left, err = strconv.Atoi(x)
  298. if err == nil {
  299. if ans.Canvas.Top, err = strconv.Atoi(y); err == nil {
  300. ok = true
  301. }
  302. }
  303. }
  304. }
  305. }
  306. }
  307. }
  308. if !ok {
  309. return fmt.Errorf("Invalid canvas value in identify output: %s", raw.Canvas)
  310. }
  311. w, h, found := strings.Cut(raw.Size, "x")
  312. ok = false
  313. if found {
  314. ans.Width, err = strconv.Atoi(w)
  315. if err == nil {
  316. if ans.Height, err = strconv.Atoi(h); err == nil {
  317. ok = true
  318. }
  319. }
  320. }
  321. if !ok {
  322. return fmt.Errorf("Invalid size value in identify output: %s", raw.Size)
  323. }
  324. x, y, found := strings.Cut(raw.Dpi, "x")
  325. ok = false
  326. if found {
  327. ans.Dpi.X, err = strconv.ParseFloat(x, 64)
  328. if err == nil {
  329. if ans.Dpi.Y, err = strconv.ParseFloat(y, 64); err == nil {
  330. ok = true
  331. }
  332. }
  333. }
  334. if !ok {
  335. return fmt.Errorf("Invalid dpi value in identify output: %s", raw.Dpi)
  336. }
  337. ans.Index, err = strconv.Atoi(raw.Index)
  338. if err != nil {
  339. return fmt.Errorf("Invalid index value in identify output: %s", raw.Index)
  340. }
  341. q := strings.ToLower(raw.Transparency)
  342. if q == "blend" || q == "true" {
  343. ans.Is_opaque = false
  344. } else {
  345. ans.Is_opaque = true
  346. }
  347. ans.Needs_blend = q == "blend"
  348. switch strings.ToLower(raw.Dispose) {
  349. case "undefined":
  350. ans.Disposal = 0
  351. case "none":
  352. ans.Disposal = gif.DisposalNone
  353. case "background":
  354. ans.Disposal = gif.DisposalBackground
  355. case "previous":
  356. ans.Disposal = gif.DisposalPrevious
  357. default:
  358. return fmt.Errorf("Invalid value for dispose: %s", raw.Dispose)
  359. }
  360. switch raw.Orientation {
  361. case "5", "6", "7", "8":
  362. ans.Dimensions_swapped = true
  363. }
  364. if ans.Dimensions_swapped {
  365. ans.Canvas.Width, ans.Canvas.Height = ans.Canvas.Height, ans.Canvas.Width
  366. ans.Width, ans.Height = ans.Height, ans.Width
  367. }
  368. return
  369. }
  370. func IdentifyWithMagick(path string) (ans []IdentifyRecord, err error) {
  371. cmd := []string{"identify"}
  372. q := `{"fmt":"%m","canvas":"%g","transparency":"%A","gap":"%T","index":"%p","size":"%wx%h",` +
  373. `"dpi":"%xx%y","dispose":"%D","orientation":"%[EXIF:Orientation]"},`
  374. cmd = append(cmd, "-format", q, "--", path)
  375. output, err := RunMagick(path, cmd)
  376. if err != nil {
  377. return nil, fmt.Errorf("Failed to identify image at path: %s with error: %w", path, err)
  378. }
  379. output = bytes.TrimRight(bytes.TrimSpace(output), ",")
  380. raw_json := make([]byte, 0, len(output)+2)
  381. raw_json = append(raw_json, '[')
  382. raw_json = append(raw_json, output...)
  383. raw_json = append(raw_json, ']')
  384. var records []IdentifyOutput
  385. err = json.Unmarshal(raw_json, &records)
  386. if err != nil {
  387. return nil, fmt.Errorf("The ImageMagick identify program returned malformed output for the image at path: %s, with error: %w", path, err)
  388. }
  389. ans = make([]IdentifyRecord, len(records))
  390. for i, rec := range records {
  391. err = parse_identify_record(&ans[i], &rec)
  392. if err != nil {
  393. return nil, err
  394. }
  395. }
  396. return ans, nil
  397. }
  398. type RenderOptions struct {
  399. RemoveAlpha *NRGBColor
  400. Flip, Flop bool
  401. ResizeTo image.Point
  402. OnlyFirstFrame bool
  403. TempfilenameTemplate string
  404. }
  405. func RenderWithMagick(path string, ro *RenderOptions, frames []IdentifyRecord) (ans []*ImageFrame, fmap map[int]string, err error) {
  406. cmd := []string{"convert"}
  407. ans = make([]*ImageFrame, 0, len(frames))
  408. fmap = make(map[int]string, len(frames))
  409. defer func() {
  410. if err != nil {
  411. for _, f := range fmap {
  412. os.Remove(f)
  413. }
  414. }
  415. }()
  416. if ro.RemoveAlpha != nil {
  417. cmd = append(cmd, "-background", ro.RemoveAlpha.AsSharp(), "-alpha", "remove")
  418. } else {
  419. cmd = append(cmd, "-background", "none")
  420. }
  421. if ro.Flip {
  422. cmd = append(cmd, "-flip")
  423. }
  424. if ro.Flop {
  425. cmd = append(cmd, "-flop")
  426. }
  427. cpath := path
  428. if ro.OnlyFirstFrame {
  429. cpath += "[0]"
  430. }
  431. has_multiple_frames := len(frames) > 1
  432. get_multiple_frames := has_multiple_frames && !ro.OnlyFirstFrame
  433. cmd = append(cmd, "--", cpath, "-auto-orient")
  434. if ro.ResizeTo.X > 0 {
  435. rcmd := []string{"-resize", fmt.Sprintf("%dx%d!", ro.ResizeTo.X, ro.ResizeTo.Y)}
  436. if get_multiple_frames {
  437. cmd = append(cmd, "-coalesce")
  438. cmd = append(cmd, rcmd...)
  439. cmd = append(cmd, "-deconstruct")
  440. } else {
  441. cmd = append(cmd, rcmd...)
  442. }
  443. }
  444. cmd = append(cmd, "-depth", "8", "-set", "filename:f", "%w-%h-%g-%p")
  445. if get_multiple_frames {
  446. cmd = append(cmd, "+adjoin")
  447. }
  448. tdir, err := MakeTempDir(ro.TempfilenameTemplate)
  449. if err != nil {
  450. err = fmt.Errorf("Failed to create temporary directory to hold ImageMagick output with error: %w", err)
  451. return
  452. }
  453. defer os.RemoveAll(tdir)
  454. mode := "rgba"
  455. if frames[0].Is_opaque {
  456. mode = "rgb"
  457. }
  458. cmd = append(cmd, filepath.Join(tdir, "im-%[filename:f]."+mode))
  459. _, err = RunMagick(path, cmd)
  460. if err != nil {
  461. return
  462. }
  463. entries, err := os.ReadDir(tdir)
  464. if err != nil {
  465. err = fmt.Errorf("Failed to read temp dir used to store ImageMagick output with error: %w", err)
  466. return
  467. }
  468. base_dir := filepath.Dir(tdir)
  469. gaps := make([]int, len(frames))
  470. for i, frame := range frames {
  471. gaps[i] = frame.Gap
  472. }
  473. min_gap := CalcMinimumGIFGap(gaps)
  474. for _, entry := range entries {
  475. fname := entry.Name()
  476. p, _, _ := strings.Cut(fname, ".")
  477. parts := strings.Split(p, "-")
  478. if len(parts) < 5 {
  479. continue
  480. }
  481. index, cerr := strconv.Atoi(parts[len(parts)-1])
  482. if cerr != nil || index < 0 || index >= len(frames) {
  483. continue
  484. }
  485. width, cerr := strconv.Atoi(parts[1])
  486. if cerr != nil {
  487. continue
  488. }
  489. height, cerr := strconv.Atoi(parts[2])
  490. if cerr != nil {
  491. continue
  492. }
  493. _, pos, found := strings.Cut(parts[3], "+")
  494. if !found {
  495. continue
  496. }
  497. px, py, found := strings.Cut(pos, "+")
  498. if !found {
  499. continue
  500. }
  501. x, cerr := strconv.Atoi(px)
  502. if cerr != nil {
  503. continue
  504. }
  505. y, cerr := strconv.Atoi(py)
  506. if cerr != nil {
  507. continue
  508. }
  509. identify_data := frames[index]
  510. df, cerr := os.CreateTemp(base_dir, TempTemplate+"."+mode)
  511. if cerr != nil {
  512. err = fmt.Errorf("Failed to create a temporary file in %s with error: %w", base_dir, cerr)
  513. return
  514. }
  515. err = os.Rename(filepath.Join(tdir, fname), df.Name())
  516. if err != nil {
  517. err = fmt.Errorf("Failed to rename a temporary file in %s with error: %w", tdir, err)
  518. return
  519. }
  520. df.Close()
  521. fmap[index+1] = df.Name()
  522. frame := ImageFrame{
  523. Number: index + 1, Width: width, Height: height, Left: x, Top: y, Is_opaque: identify_data.Is_opaque,
  524. }
  525. frame.set_delay(min_gap, identify_data.Gap)
  526. err = check_resize(&frame, df.Name())
  527. if err != nil {
  528. return
  529. }
  530. ans = append(ans, &frame)
  531. }
  532. if len(ans) < len(frames) {
  533. err = fmt.Errorf("Failed to render %d out of %d frames", len(frames)-len(ans), len(frames))
  534. return
  535. }
  536. slices.SortFunc(ans, func(a, b *ImageFrame) int { return a.Number - b.Number })
  537. anchor_frame := 1
  538. for i, frame := range ans {
  539. anchor_frame, frame.Compose_onto = SetGIFFrameDisposal(frame.Number, anchor_frame, byte(frames[i].Disposal))
  540. }
  541. return
  542. }
  543. func OpenImageFromPathWithMagick(path string) (ans *ImageData, err error) {
  544. identify_records, err := IdentifyWithMagick(path)
  545. if err != nil {
  546. return nil, fmt.Errorf("Failed to identify image at %#v with error: %w", path, err)
  547. }
  548. frames, filenames, err := RenderWithMagick(path, &RenderOptions{}, identify_records)
  549. if err != nil {
  550. return nil, fmt.Errorf("Failed to render image at %#v with error: %w", path, err)
  551. }
  552. defer func() {
  553. for _, f := range filenames {
  554. os.Remove(f)
  555. }
  556. }()
  557. for _, frame := range frames {
  558. filename := filenames[frame.Number]
  559. data, err := os.ReadFile(filename)
  560. if err != nil {
  561. return nil, fmt.Errorf("Failed to read temp file for image %#v at %#v with error: %w", path, filename, err)
  562. }
  563. dest_rect := image.Rect(0, 0, frame.Width, frame.Height)
  564. if frame.Is_opaque {
  565. frame.Img = &NRGB{Pix: data, Stride: frame.Width * 3, Rect: dest_rect}
  566. } else {
  567. frame.Img = &image.NRGBA{Pix: data, Stride: frame.Width * 4, Rect: dest_rect}
  568. }
  569. }
  570. ans = &ImageData{
  571. Width: frames[0].Width, Height: frames[0].Height, Format_uppercase: identify_records[0].Fmt_uppercase, Frames: frames,
  572. }
  573. return ans, nil
  574. }
  575. func OpenImageFromPath(path string) (ans *ImageData, err error) {
  576. mt := utils.GuessMimeType(path)
  577. if DecodableImageTypes[mt] {
  578. f, err := os.Open(path)
  579. if err != nil {
  580. return nil, err
  581. }
  582. defer f.Close()
  583. ans, err = OpenNativeImageFromReader(f)
  584. if err != nil {
  585. return nil, fmt.Errorf("Failed to load image at %#v with error: %w", path, err)
  586. }
  587. } else {
  588. return OpenImageFromPathWithMagick(path)
  589. }
  590. return
  591. }