api-native-image-spec.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539
  1. 'use strict'
  2. /* eslint-disable no-unused-expressions */
  3. const {expect} = require('chai')
  4. const {nativeImage} = require('electron')
  5. const path = require('path')
  6. describe('nativeImage module', () => {
  7. const ImageFormat = {
  8. PNG: 'png',
  9. JPEG: 'jpeg'
  10. }
  11. const images = [
  12. {
  13. filename: 'logo.png',
  14. format: ImageFormat.PNG,
  15. hasAlphaChannel: true,
  16. hasDataUrl: false,
  17. width: 538,
  18. height: 190
  19. },
  20. {
  21. dataUrl: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVQYlWNgAAIAAAUAAdafFs0AAAAASUVORK5CYII=',
  22. filename: '1x1.png',
  23. format: ImageFormat.PNG,
  24. hasAlphaChannel: true,
  25. hasDataUrl: true,
  26. height: 1,
  27. width: 1
  28. },
  29. {
  30. dataUrl: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAAFklEQVQYlWP8//8/AwMDEwMDAwMDAwAkBgMBBMzldwAAAABJRU5ErkJggg==',
  31. filename: '2x2.jpg',
  32. format: ImageFormat.JPEG,
  33. hasAlphaChannel: false,
  34. hasDataUrl: true,
  35. height: 2,
  36. width: 2
  37. },
  38. {
  39. dataUrl: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAYAAABWKLW/AAAADElEQVQYlWNgIAoAAAAnAAGZWEMnAAAAAElFTkSuQmCC',
  40. filename: '3x3.png',
  41. format: ImageFormat.PNG,
  42. hasAlphaChannel: true,
  43. hasDataUrl: true,
  44. height: 3,
  45. width: 3
  46. }
  47. ]
  48. /**
  49. * @param {?string} filename
  50. * @returns {?string} Full path.
  51. */
  52. const getImagePathFromFilename = (filename) => {
  53. return (filename === null) ? null
  54. : path.join(__dirname, 'fixtures', 'assets', filename)
  55. }
  56. /**
  57. * @param {!Object} image
  58. * @param {Object} filters
  59. * @returns {boolean}
  60. */
  61. const imageMatchesTheFilters = (image, filters = null) => {
  62. if (filters === null) {
  63. return true
  64. }
  65. return Object.entries(filters)
  66. .every(([key, value]) => image[key] === value)
  67. }
  68. /**
  69. * @param {!Object} filters
  70. * @returns {!Array} A matching images list.
  71. */
  72. const getImages = (filters) => {
  73. const matchingImages = images
  74. .filter(i => imageMatchesTheFilters(i, filters))
  75. // Add `.path` property to every image.
  76. matchingImages
  77. .forEach(i => { i.path = getImagePathFromFilename(i.filename) })
  78. return matchingImages
  79. }
  80. /**
  81. * @param {!Object} filters
  82. * @returns {Object} A matching image if any.
  83. */
  84. const getImage = (filters) => {
  85. const matchingImages = getImages(filters)
  86. let matchingImage = null
  87. if (matchingImages.length > 0) {
  88. matchingImage = matchingImages[0]
  89. }
  90. return matchingImage
  91. }
  92. describe('createEmpty()', () => {
  93. it('returns an empty image', () => {
  94. const empty = nativeImage.createEmpty()
  95. expect(empty.isEmpty())
  96. expect(empty.getAspectRatio()).to.equal(1)
  97. expect(empty.toDataURL()).to.equal('data:image/png;base64,')
  98. expect(empty.toDataURL({scaleFactor: 2.0})).to.equal('data:image/png;base64,')
  99. expect(empty.getSize()).to.deep.equal({width: 0, height: 0})
  100. expect(empty.getBitmap()).to.be.empty
  101. expect(empty.getBitmap({scaleFactor: 2.0})).to.be.empty
  102. expect(empty.toBitmap()).to.be.empty
  103. expect(empty.toBitmap({scaleFactor: 2.0})).to.be.empty
  104. expect(empty.toJPEG(100)).to.be.empty
  105. expect(empty.toPNG()).to.be.empty
  106. expect(empty.toPNG({scaleFactor: 2.0})).to.be.empty
  107. if (process.platform === 'darwin') {
  108. expect(empty.getNativeHandle()).to.be.empty
  109. }
  110. })
  111. })
  112. describe('createFromBuffer(buffer, scaleFactor)', () => {
  113. it('returns an empty image when the buffer is empty', () => {
  114. expect(nativeImage.createFromBuffer(Buffer.from([])).isEmpty())
  115. })
  116. it('returns an image created from the given buffer', () => {
  117. const imageA = nativeImage.createFromPath(path.join(__dirname, 'fixtures', 'assets', 'logo.png'))
  118. const imageB = nativeImage.createFromBuffer(imageA.toPNG())
  119. expect(imageB.getSize()).to.deep.equal({width: 538, height: 190})
  120. expect(imageA.toBitmap().equals(imageB.toBitmap())).to.be.true
  121. const imageC = nativeImage.createFromBuffer(imageA.toJPEG(100))
  122. expect(imageC.getSize()).to.deep.equal({width: 538, height: 190})
  123. const imageD = nativeImage.createFromBuffer(imageA.toBitmap(),
  124. {width: 538, height: 190})
  125. expect(imageD.getSize()).to.deep.equal({width: 538, height: 190})
  126. const imageE = nativeImage.createFromBuffer(imageA.toBitmap(),
  127. {width: 100, height: 200})
  128. expect(imageE.getSize()).to.deep.equal({width: 100, height: 200})
  129. const imageF = nativeImage.createFromBuffer(imageA.toBitmap())
  130. expect(imageF.isEmpty())
  131. const imageG = nativeImage.createFromBuffer(imageA.toPNG(),
  132. {width: 100, height: 200})
  133. expect(imageG.getSize()).to.deep.equal({width: 538, height: 190})
  134. const imageH = nativeImage.createFromBuffer(imageA.toJPEG(100),
  135. {width: 100, height: 200})
  136. expect(imageH.getSize()).to.deep.equal({width: 538, height: 190})
  137. const imageI = nativeImage.createFromBuffer(imageA.toBitmap(),
  138. {width: 538, height: 190, scaleFactor: 2.0})
  139. expect(imageI.getSize()).to.deep.equal({width: 269, height: 95})
  140. })
  141. })
  142. describe('createFromDataURL(dataURL)', () => {
  143. it('returns an empty image from the empty string', () => {
  144. expect(nativeImage.createFromDataURL('').isEmpty())
  145. })
  146. it('returns an image created from the given string', () => {
  147. const imagesData = getImages({hasDataUrl: true})
  148. for (const imageData of imagesData) {
  149. const imageFromPath = nativeImage.createFromPath(imageData.path)
  150. const imageFromDataUrl = nativeImage.createFromDataURL(imageData.dataUrl)
  151. expect(imageFromDataUrl.isEmpty())
  152. expect(imageFromDataUrl.getSize()).to.deep.equal(imageFromPath.getSize())
  153. expect(imageFromDataUrl.toBitmap()).to.satisfy(
  154. bitmap => imageFromPath.toBitmap().equals(bitmap))
  155. expect(imageFromDataUrl.toDataURL()).to.equal(imageFromPath.toDataURL())
  156. }
  157. })
  158. })
  159. describe('toDataURL()', () => {
  160. it('returns a PNG data URL', () => {
  161. const imagesData = getImages({hasDataUrl: true})
  162. for (const imageData of imagesData) {
  163. const imageFromPath = nativeImage.createFromPath(imageData.path)
  164. const scaleFactors = [1.0, 2.0]
  165. for (const scaleFactor of scaleFactors) {
  166. expect(imageFromPath.toDataURL({scaleFactor})).to.equal(imageData.dataUrl)
  167. }
  168. }
  169. })
  170. it('returns a data URL at 1x scale factor by default', () => {
  171. const imageData = getImage({filename: 'logo.png'})
  172. const image = nativeImage.createFromPath(imageData.path)
  173. const imageOne = nativeImage.createFromBuffer(image.toPNG(), {
  174. width: image.getSize().width,
  175. height: image.getSize().height,
  176. scaleFactor: 2.0
  177. })
  178. expect(imageOne.getSize()).to.deep.equal(
  179. {width: imageData.width / 2, height: imageData.height / 2})
  180. const imageTwo = nativeImage.createFromDataURL(imageOne.toDataURL())
  181. expect(imageTwo.getSize()).to.deep.equal(
  182. {width: imageData.width, height: imageData.height})
  183. expect(imageOne.toBitmap().equals(imageTwo.toBitmap())).to.be.true
  184. })
  185. it('supports a scale factor', () => {
  186. const imageData = getImage({filename: 'logo.png'})
  187. const image = nativeImage.createFromPath(imageData.path)
  188. const expectedSize = {width: imageData.width, height: imageData.height}
  189. const imageFromDataUrlOne = nativeImage.createFromDataURL(
  190. image.toDataURL({scaleFactor: 1.0}))
  191. expect(imageFromDataUrlOne.getSize()).to.deep.equal(expectedSize)
  192. const imageFromDataUrlTwo = nativeImage.createFromDataURL(
  193. image.toDataURL({scaleFactor: 2.0}))
  194. expect(imageFromDataUrlTwo.getSize()).to.deep.equal(expectedSize)
  195. })
  196. })
  197. describe('toPNG()', () => {
  198. it('returns a buffer at 1x scale factor by default', () => {
  199. const imageData = getImage({filename: 'logo.png'})
  200. const imageA = nativeImage.createFromPath(imageData.path)
  201. const imageB = nativeImage.createFromBuffer(imageA.toPNG(), {
  202. width: imageA.getSize().width,
  203. height: imageA.getSize().height,
  204. scaleFactor: 2.0
  205. })
  206. expect(imageB.getSize()).to.deep.equal(
  207. {width: imageData.width / 2, height: imageData.height / 2})
  208. const imageC = nativeImage.createFromBuffer(imageB.toPNG())
  209. expect(imageC.getSize()).to.deep.equal(
  210. {width: imageData.width, height: imageData.height})
  211. expect(imageB.toBitmap().equals(imageC.toBitmap())).to.be.true
  212. })
  213. it('supports a scale factor', () => {
  214. const imageData = getImage({filename: 'logo.png'})
  215. const image = nativeImage.createFromPath(imageData.path)
  216. const imageFromBufferOne = nativeImage.createFromBuffer(
  217. image.toPNG({scaleFactor: 1.0}))
  218. expect(imageFromBufferOne.getSize()).to.deep.equal(
  219. {width: imageData.width, height: imageData.height})
  220. const imageFromBufferTwo = nativeImage.createFromBuffer(
  221. image.toPNG({scaleFactor: 2.0}), {scaleFactor: 2.0})
  222. expect(imageFromBufferTwo.getSize()).to.deep.equal(
  223. {width: imageData.width / 2, height: imageData.height / 2})
  224. })
  225. })
  226. describe('createFromPath(path)', () => {
  227. it('returns an empty image for invalid paths', () => {
  228. expect(nativeImage.createFromPath('').isEmpty())
  229. expect(nativeImage.createFromPath('does-not-exist.png').isEmpty())
  230. expect(nativeImage.createFromPath('does-not-exist.ico').isEmpty())
  231. expect(nativeImage.createFromPath(__dirname).isEmpty())
  232. expect(nativeImage.createFromPath(__filename).isEmpty())
  233. })
  234. it('loads images from paths relative to the current working directory', () => {
  235. const imagePath = `.${path.sep}${path.join('spec', 'fixtures', 'assets', 'logo.png')}`
  236. const image = nativeImage.createFromPath(imagePath)
  237. expect(image.isEmpty()).to.be.false
  238. expect(image.getSize()).to.deep.equal({width: 538, height: 190})
  239. })
  240. it('loads images from paths with `.` segments', () => {
  241. const imagePath = `${path.join(__dirname, 'fixtures')}${path.sep}.${path.sep}${path.join('assets', 'logo.png')}`
  242. const image = nativeImage.createFromPath(imagePath)
  243. expect(image.isEmpty()).to.be.false
  244. expect(image.getSize()).to.deep.equal({width: 538, height: 190})
  245. })
  246. it('loads images from paths with `..` segments', () => {
  247. const imagePath = `${path.join(__dirname, 'fixtures', 'api')}${path.sep}..${path.sep}${path.join('assets', 'logo.png')}`
  248. const image = nativeImage.createFromPath(imagePath)
  249. expect(image.isEmpty()).to.be.false
  250. expect(image.getSize()).to.deep.equal({width: 538, height: 190})
  251. })
  252. it('Gets an NSImage pointer on macOS', function () {
  253. if (process.platform !== 'darwin') {
  254. // FIXME(alexeykuzmin): Skip the test.
  255. // this.skip()
  256. return
  257. }
  258. const imagePath = `${path.join(__dirname, 'fixtures', 'api')}${path.sep}..${path.sep}${path.join('assets', 'logo.png')}`
  259. const image = nativeImage.createFromPath(imagePath)
  260. const nsimage = image.getNativeHandle()
  261. expect(nsimage).to.have.lengthOf(8)
  262. // If all bytes are null, that's Bad
  263. const allBytesAreNotNull = nsimage.reduce((acc, x) => acc || (x !== 0), false)
  264. expect(allBytesAreNotNull)
  265. })
  266. it('loads images from .ico files on Windows', function () {
  267. if (process.platform !== 'win32') {
  268. // FIXME(alexeykuzmin): Skip the test.
  269. // this.skip()
  270. return
  271. }
  272. const imagePath = path.join(__dirname, 'fixtures', 'assets', 'icon.ico')
  273. const image = nativeImage.createFromPath(imagePath)
  274. expect(image.isEmpty()).to.be.false
  275. expect(image.getSize()).to.deep.equal({width: 256, height: 256})
  276. })
  277. })
  278. describe('createFromNamedImage(name)', () => {
  279. it('returns empty for invalid options', () => {
  280. const image = nativeImage.createFromNamedImage('totally_not_real')
  281. expect(image.isEmpty())
  282. })
  283. it('returns empty on non-darwin platforms', function () {
  284. if (process.platform === 'darwin') {
  285. // FIXME(alexeykuzmin): Skip the test.
  286. // this.skip()
  287. return
  288. }
  289. const image = nativeImage.createFromNamedImage('NSActionTemplate')
  290. expect(image.isEmpty())
  291. })
  292. it('returns a valid image on darwin', function () {
  293. if (process.platform !== 'darwin') {
  294. // FIXME(alexeykuzmin): Skip the test.
  295. // this.skip()
  296. return
  297. }
  298. const image = nativeImage.createFromNamedImage('NSActionTemplate')
  299. expect(image.isEmpty()).to.be.false
  300. })
  301. it('returns allows an HSL shift for a valid image on darwin', function () {
  302. if (process.platform !== 'darwin') {
  303. // FIXME(alexeykuzmin): Skip the test.
  304. // this.skip()
  305. return
  306. }
  307. const image = nativeImage.createFromNamedImage('NSActionTemplate', [0.5, 0.2, 0.8])
  308. expect(image.isEmpty()).to.be.false
  309. })
  310. })
  311. describe('resize(options)', () => {
  312. it('returns a resized image', () => {
  313. const image = nativeImage.createFromPath(path.join(__dirname, 'fixtures', 'assets', 'logo.png'))
  314. for (const [resizeTo, expectedSize] of new Map([
  315. [{}, {width: 538, height: 190}],
  316. [{width: 269}, {width: 269, height: 95}],
  317. [{width: 600}, {width: 600, height: 212}],
  318. [{height: 95}, {width: 269, height: 95}],
  319. [{height: 200}, {width: 566, height: 200}],
  320. [{width: 80, height: 65}, {width: 80, height: 65}],
  321. [{width: 600, height: 200}, {width: 600, height: 200}],
  322. [{width: 0, height: 0}, {width: 0, height: 0}],
  323. [{width: -1, height: -1}, {width: 0, height: 0}]
  324. ])) {
  325. const actualSize = image.resize(resizeTo).getSize()
  326. expect(actualSize).to.deep.equal(expectedSize)
  327. }
  328. })
  329. it('returns an empty image when called on an empty image', () => {
  330. expect(nativeImage.createEmpty().resize({width: 1, height: 1}).isEmpty())
  331. expect(nativeImage.createEmpty().resize({width: 0, height: 0}).isEmpty())
  332. })
  333. it('supports a quality option', () => {
  334. const image = nativeImage.createFromPath(path.join(__dirname, 'fixtures', 'assets', 'logo.png'))
  335. const good = image.resize({width: 100, height: 100, quality: 'good'})
  336. const better = image.resize({width: 100, height: 100, quality: 'better'})
  337. const best = image.resize({width: 100, height: 100, quality: 'best'})
  338. expect(good.toPNG()).to.have.lengthOf.at.most(better.toPNG().length)
  339. expect(better.toPNG()).to.have.lengthOf.below(best.toPNG().length)
  340. })
  341. })
  342. describe('crop(bounds)', () => {
  343. it('returns an empty image when called on an empty image', () => {
  344. expect(nativeImage.createEmpty().crop({width: 1, height: 2, x: 0, y: 0}).isEmpty())
  345. expect(nativeImage.createEmpty().crop({width: 0, height: 0, x: 0, y: 0}).isEmpty())
  346. })
  347. it('returns an empty image when the bounds are invalid', () => {
  348. const image = nativeImage.createFromPath(path.join(__dirname, 'fixtures', 'assets', 'logo.png'))
  349. expect(image.crop({width: 0, height: 0, x: 0, y: 0}).isEmpty())
  350. expect(image.crop({width: -1, height: 10, x: 0, y: 0}).isEmpty())
  351. expect(image.crop({width: 10, height: -35, x: 0, y: 0}).isEmpty())
  352. expect(image.crop({width: 100, height: 100, x: 1000, y: 1000}).isEmpty())
  353. })
  354. it('returns a cropped image', () => {
  355. const image = nativeImage.createFromPath(path.join(__dirname, 'fixtures', 'assets', 'logo.png'))
  356. const cropA = image.crop({width: 25, height: 64, x: 0, y: 0})
  357. const cropB = image.crop({width: 25, height: 64, x: 30, y: 40})
  358. expect(cropA.getSize()).to.deep.equal({width: 25, height: 64})
  359. expect(cropB.getSize()).to.deep.equal({width: 25, height: 64})
  360. expect(cropA.toPNG().equals(cropB.toPNG())).to.be.false
  361. })
  362. })
  363. describe('getAspectRatio()', () => {
  364. it('returns an aspect ratio of an empty image', () => {
  365. expect(nativeImage.createEmpty().getAspectRatio()).to.equal(1.0)
  366. })
  367. it('returns an aspect ratio of an image', () => {
  368. const imageData = getImage({filename: 'logo.png'})
  369. // imageData.width / imageData.height = 2.831578947368421
  370. const expectedAspectRatio = 2.8315789699554443
  371. const image = nativeImage.createFromPath(imageData.path)
  372. expect(image.getAspectRatio()).to.equal(expectedAspectRatio)
  373. })
  374. })
  375. describe('addRepresentation()', () => {
  376. it('supports adding a buffer representation for a scale factor', () => {
  377. const image = nativeImage.createEmpty()
  378. const imageDataOne = getImage({width: 1, height: 1})
  379. image.addRepresentation({
  380. scaleFactor: 1.0,
  381. buffer: nativeImage.createFromPath(imageDataOne.path).toPNG()
  382. })
  383. const imageDataTwo = getImage({width: 2, height: 2})
  384. image.addRepresentation({
  385. scaleFactor: 2.0,
  386. buffer: nativeImage.createFromPath(imageDataTwo.path).toPNG()
  387. })
  388. const imageDataThree = getImage({width: 3, height: 3})
  389. image.addRepresentation({
  390. scaleFactor: 3.0,
  391. buffer: nativeImage.createFromPath(imageDataThree.path).toPNG()
  392. })
  393. image.addRepresentation({
  394. scaleFactor: 4.0,
  395. buffer: 'invalid'
  396. })
  397. expect(image.isEmpty()).to.be.false
  398. expect(image.getSize()).to.deep.equal({width: 1, height: 1})
  399. expect(image.toDataURL({scaleFactor: 1.0})).to.equal(imageDataOne.dataUrl)
  400. expect(image.toDataURL({scaleFactor: 2.0})).to.equal(imageDataTwo.dataUrl)
  401. expect(image.toDataURL({scaleFactor: 3.0})).to.equal(imageDataThree.dataUrl)
  402. expect(image.toDataURL({scaleFactor: 4.0})).to.equal(imageDataThree.dataUrl)
  403. })
  404. it('supports adding a data URL representation for a scale factor', () => {
  405. const image = nativeImage.createEmpty()
  406. const imageDataOne = getImage({width: 1, height: 1})
  407. image.addRepresentation({
  408. scaleFactor: 1.0,
  409. dataURL: imageDataOne.dataUrl
  410. })
  411. const imageDataTwo = getImage({width: 2, height: 2})
  412. image.addRepresentation({
  413. scaleFactor: 2.0,
  414. dataURL: imageDataTwo.dataUrl
  415. })
  416. const imageDataThree = getImage({width: 3, height: 3})
  417. image.addRepresentation({
  418. scaleFactor: 3.0,
  419. dataURL: imageDataThree.dataUrl
  420. })
  421. image.addRepresentation({
  422. scaleFactor: 4.0,
  423. dataURL: 'invalid'
  424. })
  425. expect(image.isEmpty()).to.be.false
  426. expect(image.getSize()).to.deep.equal({width: 1, height: 1})
  427. expect(image.toDataURL({scaleFactor: 1.0})).to.equal(imageDataOne.dataUrl)
  428. expect(image.toDataURL({scaleFactor: 2.0})).to.equal(imageDataTwo.dataUrl)
  429. expect(image.toDataURL({scaleFactor: 3.0})).to.equal(imageDataThree.dataUrl)
  430. expect(image.toDataURL({scaleFactor: 4.0})).to.equal(imageDataThree.dataUrl)
  431. })
  432. it('supports adding a representation to an existing image', () => {
  433. const imageDataOne = getImage({width: 1, height: 1})
  434. const image = nativeImage.createFromPath(imageDataOne.path)
  435. const imageDataTwo = getImage({width: 2, height: 2})
  436. image.addRepresentation({
  437. scaleFactor: 2.0,
  438. dataURL: imageDataTwo.dataUrl
  439. })
  440. const imageDataThree = getImage({width: 3, height: 3})
  441. image.addRepresentation({
  442. scaleFactor: 2.0,
  443. dataURL: imageDataThree.dataUrl
  444. })
  445. expect(image.toDataURL({scaleFactor: 1.0})).to.equal(imageDataOne.dataUrl)
  446. expect(image.toDataURL({scaleFactor: 2.0})).to.equal(imageDataTwo.dataUrl)
  447. })
  448. })
  449. })