agi.image.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680
  1. package agi
  2. import (
  3. "bytes"
  4. "encoding/base64"
  5. "encoding/json"
  6. "errors"
  7. "fmt"
  8. "image"
  9. "image/jpeg"
  10. _ "image/jpeg"
  11. "image/png"
  12. _ "image/png"
  13. "io"
  14. "os"
  15. "path/filepath"
  16. "strings"
  17. "github.com/disintegration/imaging"
  18. "github.com/oliamb/cutter"
  19. "github.com/robertkrimen/otto"
  20. "github.com/rwcarlsen/goexif/exif"
  21. "imuslab.com/arozos/mod/agi/static"
  22. "imuslab.com/arozos/mod/filesystem"
  23. "imuslab.com/arozos/mod/filesystem/arozfs"
  24. metadata "imuslab.com/arozos/mod/filesystem/metadata"
  25. "imuslab.com/arozos/mod/info/logger"
  26. "imuslab.com/arozos/mod/utils"
  27. )
  28. /*
  29. AJGI Image Processing Library
  30. This is a library for handling image related functionalities in agi scripts.
  31. */
  32. func (g *Gateway) ImageLibRegister() {
  33. err := g.RegisterLib("imagelib", g.injectImageLibFunctions)
  34. if err != nil {
  35. logger.PrintAndLog("Agi", fmt.Sprint(err), nil)
  36. os.Exit(1)
  37. }
  38. }
  39. func (g *Gateway) injectImageLibFunctions(payload *static.AgiLibInjectionPayload) {
  40. vm := payload.VM
  41. u := payload.User
  42. //scriptFsh := payload.ScriptFsh
  43. //scriptPath := payload.ScriptPath
  44. //w := payload.Writer
  45. //r := payload.Request
  46. //Get image dimension, requires filepath (virtual)
  47. vm.Set("_imagelib_getImageDimension", func(call otto.FunctionCall) otto.Value {
  48. imageFileVpath, err := call.Argument(0).ToString()
  49. if err != nil {
  50. g.RaiseError(err)
  51. return otto.FalseValue()
  52. }
  53. fsh, imagePath, err := static.VirtualPathToRealPath(imageFileVpath, u)
  54. if err != nil {
  55. g.RaiseError(err)
  56. return otto.FalseValue()
  57. }
  58. if !fsh.FileSystemAbstraction.FileExists(imagePath) {
  59. g.RaiseError(errors.New("File not exists! Given " + imagePath))
  60. return otto.FalseValue()
  61. }
  62. openingPath := imagePath
  63. var closerFunc func()
  64. var file arozfs.File
  65. if fsh.RequireBuffer {
  66. bufferPath, cf := g.getUserSpecificTempFilePath(u, imagePath)
  67. closerFunc = cf
  68. defer closerFunc()
  69. c, err := fsh.FileSystemAbstraction.ReadFile(imagePath)
  70. if err != nil {
  71. g.RaiseError(errors.New("Read from file system failed: " + err.Error()))
  72. return otto.FalseValue()
  73. }
  74. os.WriteFile(bufferPath, c, 0775)
  75. openingPath = bufferPath
  76. file, err = os.Open(openingPath)
  77. if err != nil {
  78. g.RaiseError(err)
  79. return otto.FalseValue()
  80. }
  81. } else {
  82. file, err = fsh.FileSystemAbstraction.Open(openingPath)
  83. if err != nil {
  84. g.RaiseError(err)
  85. return otto.FalseValue()
  86. }
  87. }
  88. image, _, err := image.DecodeConfig(file)
  89. if err != nil {
  90. g.RaiseError(err)
  91. return otto.FalseValue()
  92. }
  93. file.Close()
  94. rawResults := []int{image.Width, image.Height}
  95. result, _ := vm.ToValue(rawResults)
  96. return result
  97. })
  98. //Resize image, require (filepath, outputpath, width, height)
  99. vm.Set("_imagelib_resizeImage", func(call otto.FunctionCall) otto.Value {
  100. vsrc, err := call.Argument(0).ToString()
  101. if err != nil {
  102. g.RaiseError(err)
  103. return otto.FalseValue()
  104. }
  105. vdest, err := call.Argument(1).ToString()
  106. if err != nil {
  107. g.RaiseError(err)
  108. return otto.FalseValue()
  109. }
  110. width, err := call.Argument(2).ToInteger()
  111. if err != nil {
  112. g.RaiseError(err)
  113. return otto.FalseValue()
  114. }
  115. height, err := call.Argument(3).ToInteger()
  116. if err != nil {
  117. g.RaiseError(err)
  118. return otto.FalseValue()
  119. }
  120. //Convert the virtual paths to real paths
  121. srcfsh, rsrc, err := static.VirtualPathToRealPath(vsrc, u)
  122. if err != nil {
  123. g.RaiseError(err)
  124. return otto.FalseValue()
  125. }
  126. destfsh, rdest, err := static.VirtualPathToRealPath(vdest, u)
  127. if err != nil {
  128. g.RaiseError(err)
  129. return otto.FalseValue()
  130. }
  131. ext := strings.ToLower(filepath.Ext(rdest))
  132. if !utils.StringInArray([]string{".jpg", ".jpeg", ".png"}, ext) {
  133. g.RaiseError(errors.New("File extension not supported. Only support .jpg and .png"))
  134. return otto.FalseValue()
  135. }
  136. if destfsh.FileSystemAbstraction.FileExists(rdest) {
  137. err := destfsh.FileSystemAbstraction.Remove(rdest)
  138. if err != nil {
  139. g.RaiseError(err)
  140. return otto.FalseValue()
  141. }
  142. }
  143. resizeOpeningFile := rsrc
  144. resizeWritingFile := rdest
  145. var srcFile arozfs.File
  146. var destFile arozfs.File
  147. if srcfsh.RequireBuffer {
  148. resizeOpeningFile, _, err = g.bufferRemoteResourcesToLocal(srcfsh, u, rsrc)
  149. if err != nil {
  150. g.RaiseError(err)
  151. return otto.FalseValue()
  152. }
  153. srcFile, err = os.Open(resizeOpeningFile)
  154. if err != nil {
  155. g.RaiseError(err)
  156. return otto.FalseValue()
  157. }
  158. } else {
  159. srcFile, err = srcfsh.FileSystemAbstraction.Open(resizeOpeningFile)
  160. if err != nil {
  161. g.RaiseError(err)
  162. return otto.FalseValue()
  163. }
  164. }
  165. defer srcFile.Close()
  166. if destfsh.RequireBuffer {
  167. resizeWritingFile, _, err = g.bufferRemoteResourcesToLocal(destfsh, u, rdest)
  168. if err != nil {
  169. g.RaiseError(err)
  170. return otto.FalseValue()
  171. }
  172. destFile, err = os.OpenFile(resizeWritingFile, os.O_CREATE|os.O_WRONLY, 0775)
  173. if err != nil {
  174. g.RaiseError(err)
  175. return otto.FalseValue()
  176. }
  177. } else {
  178. destFile, err = destfsh.FileSystemAbstraction.OpenFile(resizeWritingFile, os.O_CREATE|os.O_WRONLY, 0775)
  179. if err != nil {
  180. g.RaiseError(err)
  181. return otto.FalseValue()
  182. }
  183. }
  184. defer destFile.Close()
  185. //Resize the image
  186. //src, err := imaging.Open(resizeOpeningFile)
  187. src, err := imaging.Decode(srcFile)
  188. if err != nil {
  189. //Opening failed
  190. g.RaiseError(err)
  191. return otto.FalseValue()
  192. }
  193. src = imaging.Resize(src, int(width), int(height), imaging.Lanczos)
  194. //err = imaging.Save(src, resizeWritingFile)
  195. f, err := imaging.FormatFromFilename(resizeWritingFile)
  196. if err != nil {
  197. g.RaiseError(err)
  198. return otto.FalseValue()
  199. }
  200. err = imaging.Encode(destFile, src, f)
  201. if err != nil {
  202. g.RaiseError(err)
  203. return otto.FalseValue()
  204. }
  205. if destfsh.RequireBuffer {
  206. c, _ := os.ReadFile(resizeWritingFile)
  207. destfsh.FileSystemAbstraction.WriteFile(rdest, c, 0775)
  208. }
  209. return otto.TrueValue()
  210. })
  211. //Resize image and return as base64 data URL, require (filepath, width, height, format)
  212. vm.Set("_imagelib_resizeImageBase64", func(call otto.FunctionCall) otto.Value {
  213. vsrc, err := call.Argument(0).ToString()
  214. if err != nil {
  215. g.RaiseError(err)
  216. return otto.FalseValue()
  217. }
  218. width, err := call.Argument(1).ToInteger()
  219. if err != nil {
  220. g.RaiseError(err)
  221. return otto.FalseValue()
  222. }
  223. height, err := call.Argument(2).ToInteger()
  224. if err != nil {
  225. g.RaiseError(err)
  226. return otto.FalseValue()
  227. }
  228. format := "jpeg"
  229. if !call.Argument(3).IsUndefined() {
  230. format, err = call.Argument(3).ToString()
  231. if err != nil {
  232. format = "jpeg"
  233. }
  234. }
  235. //Convert the virtual path to real path
  236. srcfsh, rsrc, err := static.VirtualPathToRealPath(vsrc, u)
  237. if err != nil {
  238. g.RaiseError(err)
  239. return otto.FalseValue()
  240. }
  241. resizeOpeningFile := rsrc
  242. var srcFile arozfs.File
  243. if srcfsh.RequireBuffer {
  244. resizeOpeningFile, _, err = g.bufferRemoteResourcesToLocal(srcfsh, u, rsrc)
  245. if err != nil {
  246. g.RaiseError(err)
  247. return otto.FalseValue()
  248. }
  249. srcFile, err = os.Open(resizeOpeningFile)
  250. if err != nil {
  251. g.RaiseError(err)
  252. return otto.FalseValue()
  253. }
  254. } else {
  255. srcFile, err = srcfsh.FileSystemAbstraction.Open(resizeOpeningFile)
  256. if err != nil {
  257. g.RaiseError(err)
  258. return otto.FalseValue()
  259. }
  260. }
  261. defer srcFile.Close()
  262. //Decode and resize the image
  263. src, err := imaging.Decode(srcFile)
  264. if err != nil {
  265. g.RaiseError(err)
  266. return otto.FalseValue()
  267. }
  268. src = imaging.Resize(src, int(width), int(height), imaging.Lanczos)
  269. //Encode to bytes buffer
  270. var buf bytes.Buffer
  271. if format == "png" {
  272. err = png.Encode(&buf, src)
  273. } else {
  274. err = jpeg.Encode(&buf, src, &jpeg.Options{Quality: 85})
  275. }
  276. if err != nil {
  277. g.RaiseError(err)
  278. return otto.FalseValue()
  279. }
  280. //Convert to base64
  281. imageBytes := buf.Bytes()
  282. base64String := "data:image/" + format + ";base64," + base64.StdEncoding.EncodeToString(imageBytes)
  283. result, _ := vm.ToValue(base64String)
  284. return result
  285. })
  286. //Crop the given image, require (input, output, posx, posy, width, height)
  287. vm.Set("_imagelib_cropImage", func(call otto.FunctionCall) otto.Value {
  288. vsrc, err := call.Argument(0).ToString()
  289. if err != nil {
  290. g.RaiseError(err)
  291. return otto.FalseValue()
  292. }
  293. vdest, err := call.Argument(1).ToString()
  294. if err != nil {
  295. g.RaiseError(err)
  296. return otto.FalseValue()
  297. }
  298. posx, err := call.Argument(2).ToInteger()
  299. if err != nil {
  300. posx = 0
  301. }
  302. posy, err := call.Argument(3).ToInteger()
  303. if err != nil {
  304. posy = 0
  305. }
  306. width, err := call.Argument(4).ToInteger()
  307. if err != nil {
  308. g.RaiseError(errors.New("Image width not defined"))
  309. return otto.FalseValue()
  310. }
  311. height, err := call.Argument(5).ToInteger()
  312. if err != nil {
  313. g.RaiseError(errors.New("Image height not defined"))
  314. return otto.FalseValue()
  315. }
  316. //Convert the virtual paths to realpaths
  317. srcFsh, rsrc, err := static.VirtualPathToRealPath(vsrc, u)
  318. if err != nil {
  319. g.RaiseError(err)
  320. return otto.FalseValue()
  321. }
  322. srcFshAbs := srcFsh.FileSystemAbstraction
  323. destFsh, rdest, err := static.VirtualPathToRealPath(vdest, u)
  324. if err != nil {
  325. g.RaiseError(err)
  326. return otto.FalseValue()
  327. }
  328. //Try to read the source image
  329. imageBytes, err := srcFshAbs.ReadFile(rsrc)
  330. if err != nil {
  331. fmt.Println(err)
  332. g.RaiseError(err)
  333. return otto.FalseValue()
  334. }
  335. img, _, err := image.Decode(bytes.NewReader(imageBytes))
  336. if err != nil {
  337. g.RaiseError(err)
  338. return otto.FalseValue()
  339. }
  340. //Crop the image
  341. croppedImg, _ := cutter.Crop(img, cutter.Config{
  342. Width: int(width),
  343. Height: int(height),
  344. Anchor: image.Point{int(posx), int(posy)},
  345. Mode: cutter.TopLeft,
  346. })
  347. //Create the output file
  348. var out arozfs.File
  349. destWritePath := ""
  350. if destFsh.RequireBuffer {
  351. destWritePath, _ = g.getUserSpecificTempFilePath(u, rdest)
  352. //Create the new image in buffer file
  353. out, err = os.Create(destWritePath)
  354. if err != nil {
  355. g.RaiseError(err)
  356. return otto.FalseValue()
  357. }
  358. defer out.Close()
  359. } else {
  360. //Create the target file via FSA
  361. out, err = destFsh.FileSystemAbstraction.Create(rdest)
  362. if err != nil {
  363. g.RaiseError(err)
  364. return otto.FalseValue()
  365. }
  366. defer out.Close()
  367. }
  368. if strings.ToLower(filepath.Ext(rdest)) == ".png" {
  369. png.Encode(out, croppedImg)
  370. } else if strings.ToLower(filepath.Ext(rdest)) == ".jpg" {
  371. jpeg.Encode(out, croppedImg, nil)
  372. } else {
  373. g.RaiseError(errors.New("Not supported format: Only support jpg or png"))
  374. return otto.FalseValue()
  375. }
  376. out.Close()
  377. if destFsh.RequireBuffer {
  378. c, _ := os.ReadFile(destWritePath)
  379. err := destFsh.FileSystemAbstraction.WriteFile(rdest, c, 0775)
  380. if err != nil {
  381. fmt.Println(">", err.Error())
  382. }
  383. }
  384. return otto.TrueValue()
  385. })
  386. //Get the given file's thumbnail in base64
  387. vm.Set("_imagelib_loadThumbString", func(call otto.FunctionCall) otto.Value {
  388. vsrc, err := call.Argument(0).ToString()
  389. if err != nil {
  390. g.RaiseError(err)
  391. return otto.FalseValue()
  392. }
  393. fsh, err := u.GetFileSystemHandlerFromVirtualPath(vsrc)
  394. if err != nil {
  395. g.RaiseError(err)
  396. return otto.FalseValue()
  397. }
  398. rpath, _ := fsh.FileSystemAbstraction.VirtualPathToRealPath(vsrc, u.Username)
  399. //Get the files' thumb base64 string
  400. base64String, err := g.Option.FileSystemRender.LoadCache(fsh, rpath, false)
  401. if err != nil {
  402. return otto.FalseValue()
  403. } else {
  404. value, _ := vm.ToValue(base64String)
  405. return value
  406. }
  407. })
  408. //Check if image has EXIF
  409. vm.Set("_imagelib_hasExif", func(call otto.FunctionCall) otto.Value {
  410. imageFileVpath, err := call.Argument(0).ToString()
  411. if err != nil {
  412. g.RaiseError(err)
  413. return otto.FalseValue()
  414. }
  415. fsh, imagePath, err := static.VirtualPathToRealPath(imageFileVpath, u)
  416. if err != nil {
  417. g.RaiseError(err)
  418. return otto.FalseValue()
  419. }
  420. if !fsh.FileSystemAbstraction.FileExists(imagePath) {
  421. g.RaiseError(errors.New("File not exists! Given " + imagePath))
  422. return otto.FalseValue()
  423. }
  424. openingPath := imagePath
  425. var closerFunc func()
  426. if fsh.RequireBuffer {
  427. bufferPath, cf := g.getUserSpecificTempFilePath(u, imagePath)
  428. closerFunc = cf
  429. defer closerFunc()
  430. c, err := fsh.FileSystemAbstraction.ReadFile(imagePath)
  431. if err != nil {
  432. g.RaiseError(errors.New("Read from file system failed: " + err.Error()))
  433. return otto.FalseValue()
  434. }
  435. os.WriteFile(bufferPath, c, 0775)
  436. openingPath = bufferPath
  437. }
  438. //Check for EXIF
  439. var reader io.Reader
  440. if fsh.RequireBuffer {
  441. file, err := os.Open(openingPath)
  442. if err != nil {
  443. return otto.FalseValue()
  444. }
  445. defer file.Close()
  446. reader = file
  447. } else {
  448. file, err := fsh.FileSystemAbstraction.Open(openingPath)
  449. if err != nil {
  450. return otto.FalseValue()
  451. }
  452. defer file.Close()
  453. reader = file
  454. }
  455. _, err = exif.Decode(reader)
  456. if err != nil {
  457. return otto.FalseValue()
  458. }
  459. return otto.TrueValue()
  460. })
  461. //Get EXIF data as JSON
  462. vm.Set("_imagelib_getExif", func(call otto.FunctionCall) otto.Value {
  463. imageFileVpath, err := call.Argument(0).ToString()
  464. if err != nil {
  465. g.RaiseError(err)
  466. return otto.FalseValue()
  467. }
  468. fsh, imagePath, err := static.VirtualPathToRealPath(imageFileVpath, u)
  469. if err != nil {
  470. g.RaiseError(err)
  471. return otto.FalseValue()
  472. }
  473. if !fsh.FileSystemAbstraction.FileExists(imagePath) {
  474. g.RaiseError(errors.New("File not exists! Given " + imagePath))
  475. return otto.FalseValue()
  476. }
  477. openingPath := imagePath
  478. var closerFunc func()
  479. if fsh.RequireBuffer {
  480. bufferPath, cf := g.getUserSpecificTempFilePath(u, imagePath)
  481. closerFunc = cf
  482. defer closerFunc()
  483. c, err := fsh.FileSystemAbstraction.ReadFile(imagePath)
  484. if err != nil {
  485. g.RaiseError(errors.New("Read from file system failed: " + err.Error()))
  486. return otto.FalseValue()
  487. }
  488. os.WriteFile(bufferPath, c, 0775)
  489. openingPath = bufferPath
  490. }
  491. //Extract EXIF
  492. var reader io.Reader
  493. if fsh.RequireBuffer {
  494. file, err := os.Open(openingPath)
  495. if err != nil {
  496. g.RaiseError(err)
  497. return otto.FalseValue()
  498. }
  499. defer file.Close()
  500. reader = file
  501. } else {
  502. file, err := fsh.FileSystemAbstraction.Open(openingPath)
  503. if err != nil {
  504. g.RaiseError(err)
  505. return otto.FalseValue()
  506. }
  507. defer file.Close()
  508. reader = file
  509. }
  510. x, err := exif.Decode(reader)
  511. if err != nil {
  512. g.RaiseError(err)
  513. return otto.FalseValue()
  514. }
  515. exifInfo := make(map[string]interface{})
  516. exifString := x.String()
  517. lines := strings.Split(exifString, "\n")
  518. for _, line := range lines {
  519. if strings.Contains(line, ": ") {
  520. parts := strings.SplitN(line, ": ", 2)
  521. if len(parts) == 2 {
  522. exifInfo[parts[0]] = parts[1]
  523. }
  524. }
  525. }
  526. jsonBytes, err := json.Marshal(exifInfo)
  527. if err != nil {
  528. g.RaiseError(err)
  529. return otto.FalseValue()
  530. }
  531. result, _ := vm.ToValue(string(jsonBytes))
  532. return result
  533. })
  534. //Convert a RAW photo (ARW/CR2/DNG/NEF/RAF/ORF) to a normal JPEG, require (input, output)
  535. vm.Set("_imagelib_rawToJPEG", func(call otto.FunctionCall) otto.Value {
  536. vsrc, err := call.Argument(0).ToString()
  537. if err != nil {
  538. g.RaiseError(err)
  539. return otto.FalseValue()
  540. }
  541. vdest, err := call.Argument(1).ToString()
  542. if err != nil {
  543. g.RaiseError(err)
  544. return otto.FalseValue()
  545. }
  546. //Convert the virtual paths to real paths
  547. srcfsh, rsrc, err := static.VirtualPathToRealPath(vsrc, u)
  548. if err != nil {
  549. g.RaiseError(err)
  550. return otto.FalseValue()
  551. }
  552. destfsh, rdest, err := static.VirtualPathToRealPath(vdest, u)
  553. if err != nil {
  554. g.RaiseError(err)
  555. return otto.FalseValue()
  556. }
  557. if !srcfsh.FileSystemAbstraction.FileExists(rsrc) {
  558. g.RaiseError(errors.New("File not exists! Given " + rsrc))
  559. return otto.FalseValue()
  560. }
  561. err = convertRawToJPEG(srcfsh, rsrc, destfsh, rdest)
  562. if err != nil {
  563. g.RaiseError(err)
  564. return otto.FalseValue()
  565. }
  566. return otto.TrueValue()
  567. })
  568. //Wrap all the native code function into an imagelib class
  569. vm.Run(`
  570. var imagelib = {};
  571. imagelib.getImageDimension = _imagelib_getImageDimension;
  572. imagelib.resizeImage = _imagelib_resizeImage;
  573. imagelib.resizeImageBase64 = _imagelib_resizeImageBase64;
  574. imagelib.cropImage = _imagelib_cropImage;
  575. imagelib.loadThumbString = _imagelib_loadThumbString;
  576. imagelib.hasExif = _imagelib_hasExif;
  577. imagelib.getExif = _imagelib_getExif;
  578. imagelib.rawToJPEG = _imagelib_rawToJPEG;
  579. `)
  580. }
  581. // convertRawToJPEG renders a RAW photo (ARW, CR2, DNG, NEF, RAF, ORF) into a
  582. // standard JPEG by extracting its embedded full-size preview, then writes the
  583. // result to destRpath on destFsh. It returns an error when the source is not a
  584. // supported RAW format, when the output is not a .jpg/.jpeg file, or when any
  585. // read / render / write step fails. WriteFile is used for the output so this
  586. // works on every filesystem abstraction, including buffered ones (S3, FTP, WebDAV).
  587. func convertRawToJPEG(srcFsh *filesystem.FileSystemHandler, srcRpath string, destFsh *filesystem.FileSystemHandler, destRpath string) error {
  588. if !metadata.IsRawImageFile(srcRpath) {
  589. return errors.New("source is not a supported RAW image: " + filepath.Base(srcRpath))
  590. }
  591. destExt := strings.ToLower(filepath.Ext(destRpath))
  592. if destExt != ".jpg" && destExt != ".jpeg" {
  593. return errors.New("output file must have a .jpg or .jpeg extension")
  594. }
  595. jpegData, err := metadata.RenderRAWImage(srcFsh, srcRpath)
  596. if err != nil {
  597. return err
  598. }
  599. return destFsh.FileSystemAbstraction.WriteFile(destRpath, jpegData, 0775)
  600. }