diff --git a/src/core/const.js b/src/core/const.js index 4d4e9bf..4976abc 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -274,6 +274,37 @@ }, /** + * Regexp for image type by extension. + * + * @static + * @constant + * @type {RegExp|string} + * @example `image.png` + */ + IMAGE_TYPE: /\.(gif|jpe?g|tiff|png|svg)$/i, + + /** + * Regexp for data URI. + * Based on: https://github.com/ragingwind/data-uri-regex + * + * @static + * @constant + * @type {RegExp|string} + * @example `data:image/png;base64` + */ + DATA_URI: /^\s*data:(?:([\w-]+)\/([\w+.-]+))?(?:;(charset=[\w-]+|base64))?,(.*)/i, + + /** + * Regexp for SVG size. + * + * @static + * @constant + * @type {RegExp|string} + * @example `` + */ + SVG_SIZE: /]*(?:\s(width|height)="(\d*(?:\.\d+)?)(?:px)?")[^>]*(?:\s(width|height)="(\d*(?:\.\d+)?)(?:px)?")[^>]*>/i, + + /** * Constants that identify shapes, mainly to prevent `instanceof` calls. * * @static diff --git a/src/core/const.js b/src/core/const.js index 4d4e9bf..4976abc 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -274,6 +274,37 @@ }, /** + * Regexp for image type by extension. + * + * @static + * @constant + * @type {RegExp|string} + * @example `image.png` + */ + IMAGE_TYPE: /\.(gif|jpe?g|tiff|png|svg)$/i, + + /** + * Regexp for data URI. + * Based on: https://github.com/ragingwind/data-uri-regex + * + * @static + * @constant + * @type {RegExp|string} + * @example `data:image/png;base64` + */ + DATA_URI: /^\s*data:(?:([\w-]+)\/([\w+.-]+))?(?:;(charset=[\w-]+|base64))?,(.*)/i, + + /** + * Regexp for SVG size. + * + * @static + * @constant + * @type {RegExp|string} + * @example `` + */ + SVG_SIZE: /]*(?:\s(width|height)="(\d*(?:\.\d+)?)(?:px)?")[^>]*(?:\s(width|height)="(\d*(?:\.\d+)?)(?:px)?")[^>]*>/i, + + /** * Constants that identify shapes, mainly to prevent `instanceof` calls. * * @static diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 0938a65..a74c228 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -107,6 +107,33 @@ this.source = null; // set in loadSource, if at all /** + * The image source that is used to create the texture. This is used to + * store the original Svg source when it is replaced with a canvas element. + * + * TODO: Currently not in use but could be used when re-scaling svg. + * + * @member {Image} + * @readonly + */ + this.origSource = null; // set in loadSvg, if at all + + /** + * Type of image defined in source, eg. `png` or `svg` + * + * @member {string} + * @readonly + */ + this.imageType = null; // set in updateImageType + + /** + * Scale for source image. Used with Svg images to scale them before rasterization. + * + * @member {number} + * @readonly + */ + this.sourceScale = 1.0; + + /** * Controls if RGB channels should be pre-multiplied by Alpha (WebGL only) * All blend modes, and shaders written for default value. Change it on your own risk. * @@ -190,13 +217,17 @@ */ update() { - this.realWidth = this.source.naturalWidth || this.source.videoWidth || this.source.width; - this.realHeight = this.source.naturalHeight || this.source.videoHeight || this.source.height; + // Svg size is handled during load + if (this.imageType !== 'svg') + { + this.realWidth = this.source.naturalWidth || this.source.videoWidth || this.source.width; + this.realHeight = this.source.naturalHeight || this.source.videoHeight || this.source.height; - this.width = this.realWidth / this.resolution; - this.height = this.realHeight / this.resolution; + this.width = this.realWidth / this.resolution; + this.height = this.realHeight / this.resolution; - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } this.emit('update', this); } @@ -237,9 +268,18 @@ this.source = source; // Apply source if loaded. Otherwise setup appropriate loading monitors. - if ((this.source.complete || this.source.getContext) && this.source.width && this.source.height) + if ((source.src && source.complete || source.getContext) && source.width && source.height) { - this._sourceLoaded(); + this._updateImageType(); + + if (this.imageType === 'svg') + { + this._loadSvgSource(); + } + else + { + this._sourceLoaded(); + } } else if (!source.getContext) { @@ -251,6 +291,7 @@ source.onload = function () { + scope._updateImageType(); source.onload = null; source.onerror = null; @@ -262,6 +303,12 @@ scope.isLoading = false; scope._sourceLoaded(); + if (scope.imageType === 'svg') + { + scope._loadSvgSource(); + return; + } + scope.emit('loaded', scope); }; @@ -285,12 +332,19 @@ // NOTE: complete will be true if the image has no src so best to check if the src is set. if (source.complete && source.src) { - this.isLoading = false; // ..and if we're complete now, no need for callbacks source.onload = null; source.onerror = null; + if (scope.imageType === 'svg') + { + scope._loadSvgSource(); + return; + } + + this.isLoading = false; + if (source.width && source.height) { this._sourceLoaded(); @@ -314,6 +368,180 @@ } /** + * Updates type of the source image. + */ + _updateImageType() + { + if (!this.imageUrl) + { + return; + } + + const dataUri = utils.decomposeDataUri(this.imageUrl); + let imageType; + + if (dataUri && dataUri.mediaType === 'image') + { + // Check for subType validity + const firstSubType = dataUri.subType.split('+')[0]; + imageType = utils.getImageTypeOfUrl('.' + firstSubType); + + if (!imageType) + { + throw new Error('Invalid image type in data URI.'); + } + } + else + { + imageType = utils.getImageTypeOfUrl(this.imageUrl); + + if (!imageType) + { + throw new Error('Invalid image type in URL.'); + } + } + + this.imageType = imageType; + } + + /** + * Checks if `source` is an SVG image and whether it's loaded via a URL or a data URI. Then calls + * `_loadSvgSourceUsingDataUri` or `_loadSvgSourceUsingXhr`. + */ + _loadSvgSource() + { + if (this.imageType !== 'svg') + { + // Do nothing if source is not svg + return; + } + + const dataUri = utils.decomposeDataUri(this.imageUrl); + + if (dataUri) + { + this._loadSvgSourceUsingDataUri(dataUri); + } + else + { + // We got an URL, so we need to do an XHR to check the svg size + this._loadSvgSourceUsingXhr(); + } + } + + /** + * Reads an SVG string from data URI and then calls `_loadSvgSourceUsingString`. + */ + _loadSvgSourceUsingDataUri(dataUri) + { + let svgString; + + if (dataUri.encoding === 'base64') + { + if (!atob) + { + throw new Error('Your browser doesn\'t support base64 conversions.'); + } + svgString = atob(dataUri.data); + } + else + { + svgString = dataUri.data; + } + + this._loadSvgSourceUsingString(svgString); + } + + /** + * Loads an SVG string from `imageUrl` using XHR and then calls `_loadSvgSourceUsingString`. + */ + _loadSvgSourceUsingXhr() + { + const svgXhr = new XMLHttpRequest(); + + // This throws error on IE, so SVG Document can't be used + // svgXhr.responseType = 'document'; + + // This is not needed since we load the svg as string (breaks IE too) + // but overrideMimeType() can be used to force the response to be parsed as XML + // svgXhr.overrideMimeType('image/svg+xml'); + + const scope = this; + + svgXhr.onload = function () + { + if (svgXhr.readyState !== svgXhr.DONE || svgXhr.status !== 200) + { + throw new Error('Failed to load SVG using XHR.'); + } + + scope._loadSvgSourceUsingString(svgXhr.response); + }; + + svgXhr.onerror = function () + { + scope.emit('error', scope); + }; + + svgXhr.open('GET', this.imageUrl, true); + svgXhr.send(); + } + + /** + * Loads texture using an SVG string. The original SVG Image is stored as `origSource` and the + * created canvas is the new `source`. The SVG is scaled using `sourceScale`. Called by + * `_loadSvgSourceUsingXhr` or `_loadSvgSourceUsingDataUri`. + * + * @param {string} svgString SVG source as string + * + * @fires loaded + */ + _loadSvgSourceUsingString(svgString) + { + const svgSize = utils.getSvgSize(svgString); + + const svgWidth = svgSize.width; + const svgHeight = svgSize.height; + + if (!svgWidth || !svgHeight) + { + throw new Error('The SVG image must have width and height defined (in pixels), canvas API needs them.'); + } + + // Scale realWidth and realHeight + this.realWidth = Math.round(svgWidth * this.sourceScale); + this.realHeight = Math.round(svgHeight * this.sourceScale); + + this.width = this.realWidth / this.resolution; + this.height = this.realHeight / this.resolution; + + // Check pow2 after scale + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + + // Create a canvas element + const canvas = document.createElement('canvas'); + canvas.width = this.realWidth; + canvas.height = this.realHeight; + canvas._pixiId = 'canvas_' + utils.uid(); + + // Draw the Svg to the canvas + canvas + .getContext('2d') + .drawImage(this.source, 0, 0, svgWidth, svgHeight, 0, 0, this.realWidth, this.realHeight); + + // Replace the original source image with the canvas + this.origSource = this.source; + this.source = canvas; + + // Add also the canvas in cache (destroy clears by `imageUrl` and `source._pixiId`) + utils.BaseTextureCache[canvas._pixiId] = this; + + this.isLoading = false; + this._sourceLoaded(); + this.emit('loaded', this); + } + + /** * Used internally to update the width, height, and some other tracking vars once * a source has successfully loaded. * @@ -343,7 +571,8 @@ this.source.src = ''; } } - else if (this.source && this.source._pixiId) + // An svg source has both `imageUrl` and `__pixiId`, so no `else if` here + if (this.source && this.source._pixiId) { delete utils.BaseTextureCache[this.source._pixiId]; } @@ -388,9 +617,10 @@ * @param imageUrl {string} The image url of the texture * @param [crossorigin=(auto)] {boolean} Should use anonymous CORS? Defaults to true if the URL is not a data-URI. * @param [scaleMode=PIXI.SCALE_MODES.DEFAULT] {number} See {@link PIXI.SCALE_MODES} for possible values + * @param [sourceScale=(auto)] {number} Scale for the original image, used with Svg images. * @return PIXI.BaseTexture */ - static fromImage(imageUrl, crossorigin, scaleMode) + static fromImage(imageUrl, crossorigin, scaleMode, sourceScale) { let baseTexture = utils.BaseTextureCache[imageUrl]; @@ -409,12 +639,17 @@ baseTexture = new BaseTexture(image, scaleMode); baseTexture.imageUrl = imageUrl; - image.src = imageUrl; - - utils.BaseTextureCache[imageUrl] = baseTexture; + if (sourceScale) + { + baseTexture.sourceScale = sourceScale; + } // if there is an @2x at the end of the url we are going to assume its a highres image baseTexture.resolution = utils.getResolutionOfUrl(imageUrl); + + image.src = imageUrl; // Setting this triggers load + + utils.BaseTextureCache[imageUrl] = baseTexture; } return baseTexture; diff --git a/src/core/const.js b/src/core/const.js index 4d4e9bf..4976abc 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -274,6 +274,37 @@ }, /** + * Regexp for image type by extension. + * + * @static + * @constant + * @type {RegExp|string} + * @example `image.png` + */ + IMAGE_TYPE: /\.(gif|jpe?g|tiff|png|svg)$/i, + + /** + * Regexp for data URI. + * Based on: https://github.com/ragingwind/data-uri-regex + * + * @static + * @constant + * @type {RegExp|string} + * @example `data:image/png;base64` + */ + DATA_URI: /^\s*data:(?:([\w-]+)\/([\w+.-]+))?(?:;(charset=[\w-]+|base64))?,(.*)/i, + + /** + * Regexp for SVG size. + * + * @static + * @constant + * @type {RegExp|string} + * @example `` + */ + SVG_SIZE: /]*(?:\s(width|height)="(\d*(?:\.\d+)?)(?:px)?")[^>]*(?:\s(width|height)="(\d*(?:\.\d+)?)(?:px)?")[^>]*>/i, + + /** * Constants that identify shapes, mainly to prevent `instanceof` calls. * * @static diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 0938a65..a74c228 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -107,6 +107,33 @@ this.source = null; // set in loadSource, if at all /** + * The image source that is used to create the texture. This is used to + * store the original Svg source when it is replaced with a canvas element. + * + * TODO: Currently not in use but could be used when re-scaling svg. + * + * @member {Image} + * @readonly + */ + this.origSource = null; // set in loadSvg, if at all + + /** + * Type of image defined in source, eg. `png` or `svg` + * + * @member {string} + * @readonly + */ + this.imageType = null; // set in updateImageType + + /** + * Scale for source image. Used with Svg images to scale them before rasterization. + * + * @member {number} + * @readonly + */ + this.sourceScale = 1.0; + + /** * Controls if RGB channels should be pre-multiplied by Alpha (WebGL only) * All blend modes, and shaders written for default value. Change it on your own risk. * @@ -190,13 +217,17 @@ */ update() { - this.realWidth = this.source.naturalWidth || this.source.videoWidth || this.source.width; - this.realHeight = this.source.naturalHeight || this.source.videoHeight || this.source.height; + // Svg size is handled during load + if (this.imageType !== 'svg') + { + this.realWidth = this.source.naturalWidth || this.source.videoWidth || this.source.width; + this.realHeight = this.source.naturalHeight || this.source.videoHeight || this.source.height; - this.width = this.realWidth / this.resolution; - this.height = this.realHeight / this.resolution; + this.width = this.realWidth / this.resolution; + this.height = this.realHeight / this.resolution; - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } this.emit('update', this); } @@ -237,9 +268,18 @@ this.source = source; // Apply source if loaded. Otherwise setup appropriate loading monitors. - if ((this.source.complete || this.source.getContext) && this.source.width && this.source.height) + if ((source.src && source.complete || source.getContext) && source.width && source.height) { - this._sourceLoaded(); + this._updateImageType(); + + if (this.imageType === 'svg') + { + this._loadSvgSource(); + } + else + { + this._sourceLoaded(); + } } else if (!source.getContext) { @@ -251,6 +291,7 @@ source.onload = function () { + scope._updateImageType(); source.onload = null; source.onerror = null; @@ -262,6 +303,12 @@ scope.isLoading = false; scope._sourceLoaded(); + if (scope.imageType === 'svg') + { + scope._loadSvgSource(); + return; + } + scope.emit('loaded', scope); }; @@ -285,12 +332,19 @@ // NOTE: complete will be true if the image has no src so best to check if the src is set. if (source.complete && source.src) { - this.isLoading = false; // ..and if we're complete now, no need for callbacks source.onload = null; source.onerror = null; + if (scope.imageType === 'svg') + { + scope._loadSvgSource(); + return; + } + + this.isLoading = false; + if (source.width && source.height) { this._sourceLoaded(); @@ -314,6 +368,180 @@ } /** + * Updates type of the source image. + */ + _updateImageType() + { + if (!this.imageUrl) + { + return; + } + + const dataUri = utils.decomposeDataUri(this.imageUrl); + let imageType; + + if (dataUri && dataUri.mediaType === 'image') + { + // Check for subType validity + const firstSubType = dataUri.subType.split('+')[0]; + imageType = utils.getImageTypeOfUrl('.' + firstSubType); + + if (!imageType) + { + throw new Error('Invalid image type in data URI.'); + } + } + else + { + imageType = utils.getImageTypeOfUrl(this.imageUrl); + + if (!imageType) + { + throw new Error('Invalid image type in URL.'); + } + } + + this.imageType = imageType; + } + + /** + * Checks if `source` is an SVG image and whether it's loaded via a URL or a data URI. Then calls + * `_loadSvgSourceUsingDataUri` or `_loadSvgSourceUsingXhr`. + */ + _loadSvgSource() + { + if (this.imageType !== 'svg') + { + // Do nothing if source is not svg + return; + } + + const dataUri = utils.decomposeDataUri(this.imageUrl); + + if (dataUri) + { + this._loadSvgSourceUsingDataUri(dataUri); + } + else + { + // We got an URL, so we need to do an XHR to check the svg size + this._loadSvgSourceUsingXhr(); + } + } + + /** + * Reads an SVG string from data URI and then calls `_loadSvgSourceUsingString`. + */ + _loadSvgSourceUsingDataUri(dataUri) + { + let svgString; + + if (dataUri.encoding === 'base64') + { + if (!atob) + { + throw new Error('Your browser doesn\'t support base64 conversions.'); + } + svgString = atob(dataUri.data); + } + else + { + svgString = dataUri.data; + } + + this._loadSvgSourceUsingString(svgString); + } + + /** + * Loads an SVG string from `imageUrl` using XHR and then calls `_loadSvgSourceUsingString`. + */ + _loadSvgSourceUsingXhr() + { + const svgXhr = new XMLHttpRequest(); + + // This throws error on IE, so SVG Document can't be used + // svgXhr.responseType = 'document'; + + // This is not needed since we load the svg as string (breaks IE too) + // but overrideMimeType() can be used to force the response to be parsed as XML + // svgXhr.overrideMimeType('image/svg+xml'); + + const scope = this; + + svgXhr.onload = function () + { + if (svgXhr.readyState !== svgXhr.DONE || svgXhr.status !== 200) + { + throw new Error('Failed to load SVG using XHR.'); + } + + scope._loadSvgSourceUsingString(svgXhr.response); + }; + + svgXhr.onerror = function () + { + scope.emit('error', scope); + }; + + svgXhr.open('GET', this.imageUrl, true); + svgXhr.send(); + } + + /** + * Loads texture using an SVG string. The original SVG Image is stored as `origSource` and the + * created canvas is the new `source`. The SVG is scaled using `sourceScale`. Called by + * `_loadSvgSourceUsingXhr` or `_loadSvgSourceUsingDataUri`. + * + * @param {string} svgString SVG source as string + * + * @fires loaded + */ + _loadSvgSourceUsingString(svgString) + { + const svgSize = utils.getSvgSize(svgString); + + const svgWidth = svgSize.width; + const svgHeight = svgSize.height; + + if (!svgWidth || !svgHeight) + { + throw new Error('The SVG image must have width and height defined (in pixels), canvas API needs them.'); + } + + // Scale realWidth and realHeight + this.realWidth = Math.round(svgWidth * this.sourceScale); + this.realHeight = Math.round(svgHeight * this.sourceScale); + + this.width = this.realWidth / this.resolution; + this.height = this.realHeight / this.resolution; + + // Check pow2 after scale + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + + // Create a canvas element + const canvas = document.createElement('canvas'); + canvas.width = this.realWidth; + canvas.height = this.realHeight; + canvas._pixiId = 'canvas_' + utils.uid(); + + // Draw the Svg to the canvas + canvas + .getContext('2d') + .drawImage(this.source, 0, 0, svgWidth, svgHeight, 0, 0, this.realWidth, this.realHeight); + + // Replace the original source image with the canvas + this.origSource = this.source; + this.source = canvas; + + // Add also the canvas in cache (destroy clears by `imageUrl` and `source._pixiId`) + utils.BaseTextureCache[canvas._pixiId] = this; + + this.isLoading = false; + this._sourceLoaded(); + this.emit('loaded', this); + } + + /** * Used internally to update the width, height, and some other tracking vars once * a source has successfully loaded. * @@ -343,7 +571,8 @@ this.source.src = ''; } } - else if (this.source && this.source._pixiId) + // An svg source has both `imageUrl` and `__pixiId`, so no `else if` here + if (this.source && this.source._pixiId) { delete utils.BaseTextureCache[this.source._pixiId]; } @@ -388,9 +617,10 @@ * @param imageUrl {string} The image url of the texture * @param [crossorigin=(auto)] {boolean} Should use anonymous CORS? Defaults to true if the URL is not a data-URI. * @param [scaleMode=PIXI.SCALE_MODES.DEFAULT] {number} See {@link PIXI.SCALE_MODES} for possible values + * @param [sourceScale=(auto)] {number} Scale for the original image, used with Svg images. * @return PIXI.BaseTexture */ - static fromImage(imageUrl, crossorigin, scaleMode) + static fromImage(imageUrl, crossorigin, scaleMode, sourceScale) { let baseTexture = utils.BaseTextureCache[imageUrl]; @@ -409,12 +639,17 @@ baseTexture = new BaseTexture(image, scaleMode); baseTexture.imageUrl = imageUrl; - image.src = imageUrl; - - utils.BaseTextureCache[imageUrl] = baseTexture; + if (sourceScale) + { + baseTexture.sourceScale = sourceScale; + } // if there is an @2x at the end of the url we are going to assume its a highres image baseTexture.resolution = utils.getResolutionOfUrl(imageUrl); + + image.src = imageUrl; // Setting this triggers load + + utils.BaseTextureCache[imageUrl] = baseTexture; } return baseTexture; diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index eaab05f..39a2d4c 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -266,15 +266,16 @@ * @param imageUrl {string} The image url of the texture * @param [crossorigin] {boolean} Whether requests should be treated as crossorigin * @param [scaleMode=PIXI.SCALE_MODES.DEFAULT] {number} See {@link PIXI.SCALE_MODES} for possible values + * @param [sourceScale=(auto)] {number} Scale for the original image, used with SVG images. * @return {PIXI.Texture} The newly created texture */ - static fromImage(imageUrl, crossorigin, scaleMode) + static fromImage(imageUrl, crossorigin, scaleMode, sourceScale) { let texture = utils.TextureCache[imageUrl]; if (!texture) { - texture = new Texture(BaseTexture.fromImage(imageUrl, crossorigin, scaleMode)); + texture = new Texture(BaseTexture.fromImage(imageUrl, crossorigin, scaleMode, sourceScale)); utils.TextureCache[imageUrl] = texture; } diff --git a/src/core/const.js b/src/core/const.js index 4d4e9bf..4976abc 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -274,6 +274,37 @@ }, /** + * Regexp for image type by extension. + * + * @static + * @constant + * @type {RegExp|string} + * @example `image.png` + */ + IMAGE_TYPE: /\.(gif|jpe?g|tiff|png|svg)$/i, + + /** + * Regexp for data URI. + * Based on: https://github.com/ragingwind/data-uri-regex + * + * @static + * @constant + * @type {RegExp|string} + * @example `data:image/png;base64` + */ + DATA_URI: /^\s*data:(?:([\w-]+)\/([\w+.-]+))?(?:;(charset=[\w-]+|base64))?,(.*)/i, + + /** + * Regexp for SVG size. + * + * @static + * @constant + * @type {RegExp|string} + * @example `` + */ + SVG_SIZE: /]*(?:\s(width|height)="(\d*(?:\.\d+)?)(?:px)?")[^>]*(?:\s(width|height)="(\d*(?:\.\d+)?)(?:px)?")[^>]*>/i, + + /** * Constants that identify shapes, mainly to prevent `instanceof` calls. * * @static diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 0938a65..a74c228 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -107,6 +107,33 @@ this.source = null; // set in loadSource, if at all /** + * The image source that is used to create the texture. This is used to + * store the original Svg source when it is replaced with a canvas element. + * + * TODO: Currently not in use but could be used when re-scaling svg. + * + * @member {Image} + * @readonly + */ + this.origSource = null; // set in loadSvg, if at all + + /** + * Type of image defined in source, eg. `png` or `svg` + * + * @member {string} + * @readonly + */ + this.imageType = null; // set in updateImageType + + /** + * Scale for source image. Used with Svg images to scale them before rasterization. + * + * @member {number} + * @readonly + */ + this.sourceScale = 1.0; + + /** * Controls if RGB channels should be pre-multiplied by Alpha (WebGL only) * All blend modes, and shaders written for default value. Change it on your own risk. * @@ -190,13 +217,17 @@ */ update() { - this.realWidth = this.source.naturalWidth || this.source.videoWidth || this.source.width; - this.realHeight = this.source.naturalHeight || this.source.videoHeight || this.source.height; + // Svg size is handled during load + if (this.imageType !== 'svg') + { + this.realWidth = this.source.naturalWidth || this.source.videoWidth || this.source.width; + this.realHeight = this.source.naturalHeight || this.source.videoHeight || this.source.height; - this.width = this.realWidth / this.resolution; - this.height = this.realHeight / this.resolution; + this.width = this.realWidth / this.resolution; + this.height = this.realHeight / this.resolution; - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } this.emit('update', this); } @@ -237,9 +268,18 @@ this.source = source; // Apply source if loaded. Otherwise setup appropriate loading monitors. - if ((this.source.complete || this.source.getContext) && this.source.width && this.source.height) + if ((source.src && source.complete || source.getContext) && source.width && source.height) { - this._sourceLoaded(); + this._updateImageType(); + + if (this.imageType === 'svg') + { + this._loadSvgSource(); + } + else + { + this._sourceLoaded(); + } } else if (!source.getContext) { @@ -251,6 +291,7 @@ source.onload = function () { + scope._updateImageType(); source.onload = null; source.onerror = null; @@ -262,6 +303,12 @@ scope.isLoading = false; scope._sourceLoaded(); + if (scope.imageType === 'svg') + { + scope._loadSvgSource(); + return; + } + scope.emit('loaded', scope); }; @@ -285,12 +332,19 @@ // NOTE: complete will be true if the image has no src so best to check if the src is set. if (source.complete && source.src) { - this.isLoading = false; // ..and if we're complete now, no need for callbacks source.onload = null; source.onerror = null; + if (scope.imageType === 'svg') + { + scope._loadSvgSource(); + return; + } + + this.isLoading = false; + if (source.width && source.height) { this._sourceLoaded(); @@ -314,6 +368,180 @@ } /** + * Updates type of the source image. + */ + _updateImageType() + { + if (!this.imageUrl) + { + return; + } + + const dataUri = utils.decomposeDataUri(this.imageUrl); + let imageType; + + if (dataUri && dataUri.mediaType === 'image') + { + // Check for subType validity + const firstSubType = dataUri.subType.split('+')[0]; + imageType = utils.getImageTypeOfUrl('.' + firstSubType); + + if (!imageType) + { + throw new Error('Invalid image type in data URI.'); + } + } + else + { + imageType = utils.getImageTypeOfUrl(this.imageUrl); + + if (!imageType) + { + throw new Error('Invalid image type in URL.'); + } + } + + this.imageType = imageType; + } + + /** + * Checks if `source` is an SVG image and whether it's loaded via a URL or a data URI. Then calls + * `_loadSvgSourceUsingDataUri` or `_loadSvgSourceUsingXhr`. + */ + _loadSvgSource() + { + if (this.imageType !== 'svg') + { + // Do nothing if source is not svg + return; + } + + const dataUri = utils.decomposeDataUri(this.imageUrl); + + if (dataUri) + { + this._loadSvgSourceUsingDataUri(dataUri); + } + else + { + // We got an URL, so we need to do an XHR to check the svg size + this._loadSvgSourceUsingXhr(); + } + } + + /** + * Reads an SVG string from data URI and then calls `_loadSvgSourceUsingString`. + */ + _loadSvgSourceUsingDataUri(dataUri) + { + let svgString; + + if (dataUri.encoding === 'base64') + { + if (!atob) + { + throw new Error('Your browser doesn\'t support base64 conversions.'); + } + svgString = atob(dataUri.data); + } + else + { + svgString = dataUri.data; + } + + this._loadSvgSourceUsingString(svgString); + } + + /** + * Loads an SVG string from `imageUrl` using XHR and then calls `_loadSvgSourceUsingString`. + */ + _loadSvgSourceUsingXhr() + { + const svgXhr = new XMLHttpRequest(); + + // This throws error on IE, so SVG Document can't be used + // svgXhr.responseType = 'document'; + + // This is not needed since we load the svg as string (breaks IE too) + // but overrideMimeType() can be used to force the response to be parsed as XML + // svgXhr.overrideMimeType('image/svg+xml'); + + const scope = this; + + svgXhr.onload = function () + { + if (svgXhr.readyState !== svgXhr.DONE || svgXhr.status !== 200) + { + throw new Error('Failed to load SVG using XHR.'); + } + + scope._loadSvgSourceUsingString(svgXhr.response); + }; + + svgXhr.onerror = function () + { + scope.emit('error', scope); + }; + + svgXhr.open('GET', this.imageUrl, true); + svgXhr.send(); + } + + /** + * Loads texture using an SVG string. The original SVG Image is stored as `origSource` and the + * created canvas is the new `source`. The SVG is scaled using `sourceScale`. Called by + * `_loadSvgSourceUsingXhr` or `_loadSvgSourceUsingDataUri`. + * + * @param {string} svgString SVG source as string + * + * @fires loaded + */ + _loadSvgSourceUsingString(svgString) + { + const svgSize = utils.getSvgSize(svgString); + + const svgWidth = svgSize.width; + const svgHeight = svgSize.height; + + if (!svgWidth || !svgHeight) + { + throw new Error('The SVG image must have width and height defined (in pixels), canvas API needs them.'); + } + + // Scale realWidth and realHeight + this.realWidth = Math.round(svgWidth * this.sourceScale); + this.realHeight = Math.round(svgHeight * this.sourceScale); + + this.width = this.realWidth / this.resolution; + this.height = this.realHeight / this.resolution; + + // Check pow2 after scale + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + + // Create a canvas element + const canvas = document.createElement('canvas'); + canvas.width = this.realWidth; + canvas.height = this.realHeight; + canvas._pixiId = 'canvas_' + utils.uid(); + + // Draw the Svg to the canvas + canvas + .getContext('2d') + .drawImage(this.source, 0, 0, svgWidth, svgHeight, 0, 0, this.realWidth, this.realHeight); + + // Replace the original source image with the canvas + this.origSource = this.source; + this.source = canvas; + + // Add also the canvas in cache (destroy clears by `imageUrl` and `source._pixiId`) + utils.BaseTextureCache[canvas._pixiId] = this; + + this.isLoading = false; + this._sourceLoaded(); + this.emit('loaded', this); + } + + /** * Used internally to update the width, height, and some other tracking vars once * a source has successfully loaded. * @@ -343,7 +571,8 @@ this.source.src = ''; } } - else if (this.source && this.source._pixiId) + // An svg source has both `imageUrl` and `__pixiId`, so no `else if` here + if (this.source && this.source._pixiId) { delete utils.BaseTextureCache[this.source._pixiId]; } @@ -388,9 +617,10 @@ * @param imageUrl {string} The image url of the texture * @param [crossorigin=(auto)] {boolean} Should use anonymous CORS? Defaults to true if the URL is not a data-URI. * @param [scaleMode=PIXI.SCALE_MODES.DEFAULT] {number} See {@link PIXI.SCALE_MODES} for possible values + * @param [sourceScale=(auto)] {number} Scale for the original image, used with Svg images. * @return PIXI.BaseTexture */ - static fromImage(imageUrl, crossorigin, scaleMode) + static fromImage(imageUrl, crossorigin, scaleMode, sourceScale) { let baseTexture = utils.BaseTextureCache[imageUrl]; @@ -409,12 +639,17 @@ baseTexture = new BaseTexture(image, scaleMode); baseTexture.imageUrl = imageUrl; - image.src = imageUrl; - - utils.BaseTextureCache[imageUrl] = baseTexture; + if (sourceScale) + { + baseTexture.sourceScale = sourceScale; + } // if there is an @2x at the end of the url we are going to assume its a highres image baseTexture.resolution = utils.getResolutionOfUrl(imageUrl); + + image.src = imageUrl; // Setting this triggers load + + utils.BaseTextureCache[imageUrl] = baseTexture; } return baseTexture; diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index eaab05f..39a2d4c 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -266,15 +266,16 @@ * @param imageUrl {string} The image url of the texture * @param [crossorigin] {boolean} Whether requests should be treated as crossorigin * @param [scaleMode=PIXI.SCALE_MODES.DEFAULT] {number} See {@link PIXI.SCALE_MODES} for possible values + * @param [sourceScale=(auto)] {number} Scale for the original image, used with SVG images. * @return {PIXI.Texture} The newly created texture */ - static fromImage(imageUrl, crossorigin, scaleMode) + static fromImage(imageUrl, crossorigin, scaleMode, sourceScale) { let texture = utils.TextureCache[imageUrl]; if (!texture) { - texture = new Texture(BaseTexture.fromImage(imageUrl, crossorigin, scaleMode)); + texture = new Texture(BaseTexture.fromImage(imageUrl, crossorigin, scaleMode, sourceScale)); utils.TextureCache[imageUrl] = texture; } diff --git a/src/core/utils/index.js b/src/core/utils/index.js index e121d41..17a11c9 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -91,6 +91,89 @@ }, /** + * Typedef for decomposeDataUri return object. + * + * @typedef {object} DecomposedDataUri + * @property {mediaType} Media type, eg. `image` + * @property {subType} Sub type, eg. `png` + * @property {encoding} Data encoding, eg. `base64` + * @property {data} The actual data + */ + + /** + * Split a data URI into components. Returns undefined if + * parameter `dataUri` is not a valid data URI. + * + * @memberof PIXI.utils + * @param dataUri {string} the data URI to check + * @return {DecomposedDataUri|undefined} The decomposed data uri or undefined + */ + decomposeDataUri (dataUri) + { + const dataUriMatch = CONST.DATA_URI.exec(dataUri); + + if (dataUriMatch) + { + return { + mediaType: dataUriMatch[1] ? dataUriMatch[1].toLowerCase() : undefined, + subType: dataUriMatch[2] ? dataUriMatch[2].toLowerCase() : undefined, + encoding: dataUriMatch[3] ? dataUriMatch[3].toLowerCase() : undefined, + data: dataUriMatch[4] + }; + } + + return undefined; + }, + + /** + * Get type of the image by regexp for extension. Returns undefined for unknown extensions. + * + * @memberof PIXI.utils + * @param url {string} the image path + * @return {string|undefined} image extension + */ + getImageTypeOfUrl (url) + { + const extension = CONST.IMAGE_TYPE.exec(url); + + if (extension) + { + return extension[1].toLowerCase(); + } + + return undefined; + }, + + /** + * Typedef for Size object. + * + * @typedef {object} Size + * @property {width} Width component + * @property {height} Height component + */ + + /** + * Get size from an svg string using regexp. + * + * @memberof PIXI.utils + * @param svgString {string} a serialized svg element + * @return {Size|undefined} image extension + */ + getSvgSize (svgString) + { + const sizeMatch = CONST.SVG_SIZE.exec(svgString); + const size = {}; + + if (sizeMatch) + { + size[sizeMatch[1]] = Math.round(parseFloat(sizeMatch[2])); + size[sizeMatch[3]] = Math.round(parseFloat(sizeMatch[4])); + } + + return size; + }, + + /** * Logs out the version and renderer information for this running instance of PIXI. * If you don't want to see this message you can set `PIXI.utils._saidHello = true;` * so the library thinks it already said it. Keep in mind that doing that will forever diff --git a/src/core/const.js b/src/core/const.js index 4d4e9bf..4976abc 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -274,6 +274,37 @@ }, /** + * Regexp for image type by extension. + * + * @static + * @constant + * @type {RegExp|string} + * @example `image.png` + */ + IMAGE_TYPE: /\.(gif|jpe?g|tiff|png|svg)$/i, + + /** + * Regexp for data URI. + * Based on: https://github.com/ragingwind/data-uri-regex + * + * @static + * @constant + * @type {RegExp|string} + * @example `data:image/png;base64` + */ + DATA_URI: /^\s*data:(?:([\w-]+)\/([\w+.-]+))?(?:;(charset=[\w-]+|base64))?,(.*)/i, + + /** + * Regexp for SVG size. + * + * @static + * @constant + * @type {RegExp|string} + * @example `` + */ + SVG_SIZE: /]*(?:\s(width|height)="(\d*(?:\.\d+)?)(?:px)?")[^>]*(?:\s(width|height)="(\d*(?:\.\d+)?)(?:px)?")[^>]*>/i, + + /** * Constants that identify shapes, mainly to prevent `instanceof` calls. * * @static diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 0938a65..a74c228 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -107,6 +107,33 @@ this.source = null; // set in loadSource, if at all /** + * The image source that is used to create the texture. This is used to + * store the original Svg source when it is replaced with a canvas element. + * + * TODO: Currently not in use but could be used when re-scaling svg. + * + * @member {Image} + * @readonly + */ + this.origSource = null; // set in loadSvg, if at all + + /** + * Type of image defined in source, eg. `png` or `svg` + * + * @member {string} + * @readonly + */ + this.imageType = null; // set in updateImageType + + /** + * Scale for source image. Used with Svg images to scale them before rasterization. + * + * @member {number} + * @readonly + */ + this.sourceScale = 1.0; + + /** * Controls if RGB channels should be pre-multiplied by Alpha (WebGL only) * All blend modes, and shaders written for default value. Change it on your own risk. * @@ -190,13 +217,17 @@ */ update() { - this.realWidth = this.source.naturalWidth || this.source.videoWidth || this.source.width; - this.realHeight = this.source.naturalHeight || this.source.videoHeight || this.source.height; + // Svg size is handled during load + if (this.imageType !== 'svg') + { + this.realWidth = this.source.naturalWidth || this.source.videoWidth || this.source.width; + this.realHeight = this.source.naturalHeight || this.source.videoHeight || this.source.height; - this.width = this.realWidth / this.resolution; - this.height = this.realHeight / this.resolution; + this.width = this.realWidth / this.resolution; + this.height = this.realHeight / this.resolution; - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } this.emit('update', this); } @@ -237,9 +268,18 @@ this.source = source; // Apply source if loaded. Otherwise setup appropriate loading monitors. - if ((this.source.complete || this.source.getContext) && this.source.width && this.source.height) + if ((source.src && source.complete || source.getContext) && source.width && source.height) { - this._sourceLoaded(); + this._updateImageType(); + + if (this.imageType === 'svg') + { + this._loadSvgSource(); + } + else + { + this._sourceLoaded(); + } } else if (!source.getContext) { @@ -251,6 +291,7 @@ source.onload = function () { + scope._updateImageType(); source.onload = null; source.onerror = null; @@ -262,6 +303,12 @@ scope.isLoading = false; scope._sourceLoaded(); + if (scope.imageType === 'svg') + { + scope._loadSvgSource(); + return; + } + scope.emit('loaded', scope); }; @@ -285,12 +332,19 @@ // NOTE: complete will be true if the image has no src so best to check if the src is set. if (source.complete && source.src) { - this.isLoading = false; // ..and if we're complete now, no need for callbacks source.onload = null; source.onerror = null; + if (scope.imageType === 'svg') + { + scope._loadSvgSource(); + return; + } + + this.isLoading = false; + if (source.width && source.height) { this._sourceLoaded(); @@ -314,6 +368,180 @@ } /** + * Updates type of the source image. + */ + _updateImageType() + { + if (!this.imageUrl) + { + return; + } + + const dataUri = utils.decomposeDataUri(this.imageUrl); + let imageType; + + if (dataUri && dataUri.mediaType === 'image') + { + // Check for subType validity + const firstSubType = dataUri.subType.split('+')[0]; + imageType = utils.getImageTypeOfUrl('.' + firstSubType); + + if (!imageType) + { + throw new Error('Invalid image type in data URI.'); + } + } + else + { + imageType = utils.getImageTypeOfUrl(this.imageUrl); + + if (!imageType) + { + throw new Error('Invalid image type in URL.'); + } + } + + this.imageType = imageType; + } + + /** + * Checks if `source` is an SVG image and whether it's loaded via a URL or a data URI. Then calls + * `_loadSvgSourceUsingDataUri` or `_loadSvgSourceUsingXhr`. + */ + _loadSvgSource() + { + if (this.imageType !== 'svg') + { + // Do nothing if source is not svg + return; + } + + const dataUri = utils.decomposeDataUri(this.imageUrl); + + if (dataUri) + { + this._loadSvgSourceUsingDataUri(dataUri); + } + else + { + // We got an URL, so we need to do an XHR to check the svg size + this._loadSvgSourceUsingXhr(); + } + } + + /** + * Reads an SVG string from data URI and then calls `_loadSvgSourceUsingString`. + */ + _loadSvgSourceUsingDataUri(dataUri) + { + let svgString; + + if (dataUri.encoding === 'base64') + { + if (!atob) + { + throw new Error('Your browser doesn\'t support base64 conversions.'); + } + svgString = atob(dataUri.data); + } + else + { + svgString = dataUri.data; + } + + this._loadSvgSourceUsingString(svgString); + } + + /** + * Loads an SVG string from `imageUrl` using XHR and then calls `_loadSvgSourceUsingString`. + */ + _loadSvgSourceUsingXhr() + { + const svgXhr = new XMLHttpRequest(); + + // This throws error on IE, so SVG Document can't be used + // svgXhr.responseType = 'document'; + + // This is not needed since we load the svg as string (breaks IE too) + // but overrideMimeType() can be used to force the response to be parsed as XML + // svgXhr.overrideMimeType('image/svg+xml'); + + const scope = this; + + svgXhr.onload = function () + { + if (svgXhr.readyState !== svgXhr.DONE || svgXhr.status !== 200) + { + throw new Error('Failed to load SVG using XHR.'); + } + + scope._loadSvgSourceUsingString(svgXhr.response); + }; + + svgXhr.onerror = function () + { + scope.emit('error', scope); + }; + + svgXhr.open('GET', this.imageUrl, true); + svgXhr.send(); + } + + /** + * Loads texture using an SVG string. The original SVG Image is stored as `origSource` and the + * created canvas is the new `source`. The SVG is scaled using `sourceScale`. Called by + * `_loadSvgSourceUsingXhr` or `_loadSvgSourceUsingDataUri`. + * + * @param {string} svgString SVG source as string + * + * @fires loaded + */ + _loadSvgSourceUsingString(svgString) + { + const svgSize = utils.getSvgSize(svgString); + + const svgWidth = svgSize.width; + const svgHeight = svgSize.height; + + if (!svgWidth || !svgHeight) + { + throw new Error('The SVG image must have width and height defined (in pixels), canvas API needs them.'); + } + + // Scale realWidth and realHeight + this.realWidth = Math.round(svgWidth * this.sourceScale); + this.realHeight = Math.round(svgHeight * this.sourceScale); + + this.width = this.realWidth / this.resolution; + this.height = this.realHeight / this.resolution; + + // Check pow2 after scale + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + + // Create a canvas element + const canvas = document.createElement('canvas'); + canvas.width = this.realWidth; + canvas.height = this.realHeight; + canvas._pixiId = 'canvas_' + utils.uid(); + + // Draw the Svg to the canvas + canvas + .getContext('2d') + .drawImage(this.source, 0, 0, svgWidth, svgHeight, 0, 0, this.realWidth, this.realHeight); + + // Replace the original source image with the canvas + this.origSource = this.source; + this.source = canvas; + + // Add also the canvas in cache (destroy clears by `imageUrl` and `source._pixiId`) + utils.BaseTextureCache[canvas._pixiId] = this; + + this.isLoading = false; + this._sourceLoaded(); + this.emit('loaded', this); + } + + /** * Used internally to update the width, height, and some other tracking vars once * a source has successfully loaded. * @@ -343,7 +571,8 @@ this.source.src = ''; } } - else if (this.source && this.source._pixiId) + // An svg source has both `imageUrl` and `__pixiId`, so no `else if` here + if (this.source && this.source._pixiId) { delete utils.BaseTextureCache[this.source._pixiId]; } @@ -388,9 +617,10 @@ * @param imageUrl {string} The image url of the texture * @param [crossorigin=(auto)] {boolean} Should use anonymous CORS? Defaults to true if the URL is not a data-URI. * @param [scaleMode=PIXI.SCALE_MODES.DEFAULT] {number} See {@link PIXI.SCALE_MODES} for possible values + * @param [sourceScale=(auto)] {number} Scale for the original image, used with Svg images. * @return PIXI.BaseTexture */ - static fromImage(imageUrl, crossorigin, scaleMode) + static fromImage(imageUrl, crossorigin, scaleMode, sourceScale) { let baseTexture = utils.BaseTextureCache[imageUrl]; @@ -409,12 +639,17 @@ baseTexture = new BaseTexture(image, scaleMode); baseTexture.imageUrl = imageUrl; - image.src = imageUrl; - - utils.BaseTextureCache[imageUrl] = baseTexture; + if (sourceScale) + { + baseTexture.sourceScale = sourceScale; + } // if there is an @2x at the end of the url we are going to assume its a highres image baseTexture.resolution = utils.getResolutionOfUrl(imageUrl); + + image.src = imageUrl; // Setting this triggers load + + utils.BaseTextureCache[imageUrl] = baseTexture; } return baseTexture; diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index eaab05f..39a2d4c 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -266,15 +266,16 @@ * @param imageUrl {string} The image url of the texture * @param [crossorigin] {boolean} Whether requests should be treated as crossorigin * @param [scaleMode=PIXI.SCALE_MODES.DEFAULT] {number} See {@link PIXI.SCALE_MODES} for possible values + * @param [sourceScale=(auto)] {number} Scale for the original image, used with SVG images. * @return {PIXI.Texture} The newly created texture */ - static fromImage(imageUrl, crossorigin, scaleMode) + static fromImage(imageUrl, crossorigin, scaleMode, sourceScale) { let texture = utils.TextureCache[imageUrl]; if (!texture) { - texture = new Texture(BaseTexture.fromImage(imageUrl, crossorigin, scaleMode)); + texture = new Texture(BaseTexture.fromImage(imageUrl, crossorigin, scaleMode, sourceScale)); utils.TextureCache[imageUrl] = texture; } diff --git a/src/core/utils/index.js b/src/core/utils/index.js index e121d41..17a11c9 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -91,6 +91,89 @@ }, /** + * Typedef for decomposeDataUri return object. + * + * @typedef {object} DecomposedDataUri + * @property {mediaType} Media type, eg. `image` + * @property {subType} Sub type, eg. `png` + * @property {encoding} Data encoding, eg. `base64` + * @property {data} The actual data + */ + + /** + * Split a data URI into components. Returns undefined if + * parameter `dataUri` is not a valid data URI. + * + * @memberof PIXI.utils + * @param dataUri {string} the data URI to check + * @return {DecomposedDataUri|undefined} The decomposed data uri or undefined + */ + decomposeDataUri (dataUri) + { + const dataUriMatch = CONST.DATA_URI.exec(dataUri); + + if (dataUriMatch) + { + return { + mediaType: dataUriMatch[1] ? dataUriMatch[1].toLowerCase() : undefined, + subType: dataUriMatch[2] ? dataUriMatch[2].toLowerCase() : undefined, + encoding: dataUriMatch[3] ? dataUriMatch[3].toLowerCase() : undefined, + data: dataUriMatch[4] + }; + } + + return undefined; + }, + + /** + * Get type of the image by regexp for extension. Returns undefined for unknown extensions. + * + * @memberof PIXI.utils + * @param url {string} the image path + * @return {string|undefined} image extension + */ + getImageTypeOfUrl (url) + { + const extension = CONST.IMAGE_TYPE.exec(url); + + if (extension) + { + return extension[1].toLowerCase(); + } + + return undefined; + }, + + /** + * Typedef for Size object. + * + * @typedef {object} Size + * @property {width} Width component + * @property {height} Height component + */ + + /** + * Get size from an svg string using regexp. + * + * @memberof PIXI.utils + * @param svgString {string} a serialized svg element + * @return {Size|undefined} image extension + */ + getSvgSize (svgString) + { + const sizeMatch = CONST.SVG_SIZE.exec(svgString); + const size = {}; + + if (sizeMatch) + { + size[sizeMatch[1]] = Math.round(parseFloat(sizeMatch[2])); + size[sizeMatch[3]] = Math.round(parseFloat(sizeMatch[4])); + } + + return size; + }, + + /** * Logs out the version and renderer information for this running instance of PIXI. * If you don't want to see this message you can set `PIXI.utils._saidHello = true;` * so the library thinks it already said it. Keep in mind that doing that will forever diff --git a/test/core/util.js b/test/core/util.js index 3ce4726..3866135 100755 --- a/test/core/util.js +++ b/test/core/util.js @@ -59,6 +59,91 @@ // it('should return the correct resolution based on a URL'); }); + describe('decomposeDataUri', function () + { + it('should exist', function () + { + expect(PIXI.utils.decomposeDataUri) + .to.be.a('function'); + }); + + it('should decompose a data URI', function () + { + const dataUri = PIXI.utils.decomposeDataUri('data:image/png;base64,94Z9RWUN77ZW'); + expect(dataUri) + .to.be.an('object'); + expect(dataUri.mediaType) + .to.equal('image'); + expect(dataUri.subType) + .to.equal('png'); + expect(dataUri.encoding) + .to.equal('base64'); + expect(dataUri.data) + .to.equal('94Z9RWUN77ZW'); + }); + + it('should return undefined for anything else', function () + { + const dataUri = PIXI.utils.decomposeDataUri('foo'); + expect(dataUri) + .to.be.an('undefined'); + }); + }); + + describe('getImageTypeOfUrl', function () + { + it('should exist', function () + { + expect(PIXI.utils.getImageTypeOfUrl) + .to.be.a('function'); + }); + + it('should return image type of URL in lower case', function () + { + const imageType = PIXI.utils.getImageTypeOfUrl('http://foo.bar/baz.PNG'); + expect(imageType) + .to.equal('png'); + }); + }); + + describe('getSvgSize', function () + { + it('should exist', function () + { + expect(PIXI.utils.getSvgSize) + .to.be.a('function'); + }); + + it('should return a size object with width and height from an SVG string', function () + { + const svgSize = PIXI.utils.getSvgSize(''); + expect(svgSize) + .to.be.an('object'); + expect(svgSize.width) + .to.equal(64); + expect(svgSize.height) + .to.equal(32); + }); + + it('should work with px values', function () + { + const svgSize = PIXI.utils.getSvgSize(''); + expect(svgSize) + .to.be.an('object'); + expect(svgSize.width) + .to.equal(64); + expect(svgSize.height) + .to.equal(32); + }); + + it('should return an empty object when width and/or height is missing', function () + { + const svgSize = PIXI.utils.getSvgSize(''); + expect(Object.keys(svgSize).length) + .to.equal(0); + }); + }); + describe('sayHello', function () { it('should exist', function ()