diff --git a/src/services/resource-service.js b/src/services/resource-service.js new file mode 100644 index 0000000..0f0ece4 --- /dev/null +++ b/src/services/resource-service.js @@ -0,0 +1,61 @@ +/** + * The MIT License (MIT) + * + * Igor Zinken 2020 - https://www.igorski.nl + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { createCanvas } from './canvas-util'; + +const { URL } = window; + +/** + * Creates a Blob URL for the resource represented by given imageElement. + * By treating referenced Image resources as URLs to binary content, the browser + * can use its internal loading/caching mechanism when working with these resources. + * NOTE: When done using the image, don't forget to call dispose() to free allocated memory. + * + * @param {HTMLImageElement} imageElement fully loaded image + * @param {String=} type optional mime type, defaults to JPEG for photographic content + * @param {Number=} optQuality optional JPEG compression to use (when mime is JPEG) between 0 - 1 + * @return {String} Blob URL + */ +export const imageToResource = ( imageElement, type = "image/jpeg", optQuality = .9 ) => { + const { cvs, ctx } = createCanvas(); + + cvs.width = imageElement.naturalWidth || imageElement.width; + cvs.height = imageElement.naturalHeight || imageElement.height; + + ctx.drawImage( imageElement, 0, 0 ); + + return new Promise(( resolve, reject ) => { + try { + cvs.toBlob(( blob ) => { + resolve( URL.createObjectURL( blob )); + }, type, optQuality ); + } catch ( error ) { + reject( error ); + } + }); +}; + +export const disposeResource = imageBlobURL => { + URL.revokeObjectURL( imageBlobURL ); +}; + +export const verifyIfResource = imageElement => imageElement.src.startsWith( "blob:" ); diff --git a/src/store/modules/image-module.js b/src/store/modules/image-module.js index c508694..b682d21 100644 --- a/src/store/modules/image-module.js +++ b/src/store/modules/image-module.js @@ -22,7 +22,7 @@ */ import Vue from "vue"; import { - imageToResource, disposeResource, verifyIfResource + imageToResource, disposeResource, isResource } from "@/utils/resource-manager"; /** @@ -58,7 +58,7 @@ export default { * converted to binary as registered as a Blob URL. */ async addImage({ state }, { file, image, size }) { - const isValidResource = verifyIfResource( image ) || image.src.startsWith( "http" ); + const isValidResource = isResource( image ) || image.src.startsWith( "http" ); const source = isValidResource ? image.src : await imageToResource( image, file.type ); const imageData = { file, size, source }; diff --git a/src/utils/resource-manager.js b/src/utils/resource-manager.js index 0f0ece4..592d5b8 100644 --- a/src/utils/resource-manager.js +++ b/src/utils/resource-manager.js @@ -58,4 +58,4 @@ export const disposeResource = imageBlobURL => { URL.revokeObjectURL( imageBlobURL ); }; -export const verifyIfResource = imageElement => imageElement.src.startsWith( "blob:" ); +export const isResource = imageElement => imageElement.src.startsWith( "blob:" ); diff --git a/tests/unit/store/modules/image-module.spec.js b/tests/unit/store/modules/image-module.spec.js index aa38ec5..c276a25 100644 --- a/tests/unit/store/modules/image-module.spec.js +++ b/tests/unit/store/modules/image-module.spec.js @@ -6,6 +6,7 @@ let mockUpdateFn; jest.mock("@/utils/resource-manager", () => ({ imageToResource: (...args) => mockUpdateFn?.( "imageToResource", ...args ), disposeResource: (...args) => mockUpdateFn?.( "disposeResource", ...args ), + isResource: (...args) => mockUpdateFn?.( "isResource", ...args ), })); describe( "Vuex image module", () => { @@ -41,6 +42,7 @@ describe( "Vuex image module", () => { image: { src: "blob:http://foo" }, size: { width: 100, height: 100 }, }; + mockUpdateFn = jest.fn(fn => fn === "isResource" ? true : false ); const image = await actions.addImage({ state }, input ); // assert image has been added to list expect( state.images ).toEqual([ { file: input.file, size: input.size, source: input.image.src } ]); @@ -70,14 +72,13 @@ describe( "Vuex image module", () => { const state = { images: [] }; const input = { file: { type: "image/png" }, - image: new Image(), + image: { src: "base64" }, size: { width: 100, height: 100 }, }; - const blobURL = "blob:http://foo"; - mockUpdateFn = jest.fn(() => blobURL ); + mockUpdateFn = jest.fn( fn => fn === "isResource" ? false : "" ); const image = await actions.addImage({ state }, input ); // assert image has been added to list - expect( state.images ).toEqual([ { file: input.file, size: input.size, source: blobURL } ]); + expect( state.images ).toEqual([ { file: input.file, size: input.size, source: expect.any( String ) } ]); // assert image data has been allocated as Blob expect( mockUpdateFn ).toHaveBeenCalledWith( "imageToResource", input.image, input.file.type ); // assert return data contains allocated Blob resource