1/*
2 * Copyright (C) 2013 Apple Inc. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
6 * are met:
7 * 1. Redistributions of source code must retain the above copyright
8 *    notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 *    notice, this list of conditions and the following disclaimer in the
11 *    documentation and/or other materials provided with the distribution.
12 *
13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23 * THE POSSIBILITY OF SUCH DAMAGE.
24 */
25
26// Bump this version when making changes that affect the storage format.
27const _imageStorageFormatVersion = 1;
28
29// Use as a default where an image version is not otherwise specified.
30// Bump the base version when making changes that affect the result image.
31const baseDefaultImageVersion = 4;
32const defaultImageVersion = baseDefaultImageVersion + 0.01 * WebInspector.Platform.version.base + 0.0001 * WebInspector.Platform.version.release;
33
34try {
35    var _generatedImageCacheDatabase = openDatabase("com.apple.WebInspector", 1, "Web Inspector Storage Database", 5 * 1024 * 1024);
36} catch (e) {
37    // If we can't open the database it isn't the end of the world, we just will always generate
38    // the images and not cache them for better load times.
39    console.warn("Can't open database due to: " + e + ". Images will be generated instead of loaded from cache.");
40}
41
42var _initialPrefetchComplete = false;
43var _fetchedCachedImages = {};
44
45var _generatedImageUpdateFunctions = [];
46
47_prefetchCachedImagesAndUpdate();
48
49// Updates each image when the device pixel ratio changes to redraw at the new resolution.
50window.matchMedia("(-webkit-device-pixel-ratio: 1)").addListener(_devicePixelRatioChanged);
51
52function _devicePixelRatioChanged()
53{
54    _prefetchCachedImagesAndUpdate();
55}
56
57function _registerGeneratedImageUpdateFunction(update)
58{
59    console.assert(typeof update === "function");
60
61    _generatedImageUpdateFunctions.push(update);
62
63    if (_initialPrefetchComplete)
64        update();
65}
66
67function _logSQLError(tx, error)
68{
69    console.error(error.code, error.message);
70}
71
72function _logSQLTransactionError(error)
73{
74    console.error(error.code, error.message);
75}
76
77function _prefetchCachedImagesAndUpdate()
78{
79    _fetchedCachedImages = {};
80
81    function complete()
82    {
83        _initialPrefetchComplete = true;
84
85        for (var i = 0; i < _generatedImageUpdateFunctions.length; ++i)
86            _generatedImageUpdateFunctions[i]();
87    }
88
89    if (!_generatedImageCacheDatabase) {
90        complete();
91        return;
92    }
93
94    _generatedImageCacheDatabase.transaction(function(tx) {
95        tx.executeSql("SELECT key, imageVersion, data FROM CachedImages WHERE pixelRatio = ? AND formatVersion = ?", [window.devicePixelRatio, _imageStorageFormatVersion], function(tx, result) {
96            for (var i = 0; i < result.rows.length; ++i) {
97                var row = result.rows.item(i);
98                _fetchedCachedImages[row.key] = {data: row.data, imageVersion: row.imageVersion};
99            }
100
101            complete();
102        }, function(tx, error) {
103            // The select failed. That could be because the schema changed or this is the first time.
104            // Drop the table and recreate it fresh.
105
106            tx.executeSql("DROP TABLE IF EXISTS CachedImages");
107            tx.executeSql("CREATE TABLE CachedImages (key TEXT, pixelRatio INTEGER, formatVersion NUMERIC, imageVersion NUMERIC, data BLOB, UNIQUE(key, pixelRatio))", [], null, _logSQLError);
108
109            complete();
110        });
111    }, _logSQLTransactionError);
112}
113
114function platformImagePath(fileName)
115{
116    if (WebInspector.Platform.isLegacyMacOS)
117        return "Images/Legacy/" + fileName;
118    return "Images/" + fileName;
119}
120
121function saveImageToStorage(storageKey, context, width, height, imageVersion)
122{
123    console.assert(storageKey);
124    console.assert(context);
125    console.assert(typeof width === "number");
126    console.assert(typeof height === "number");
127    console.assert(typeof imageVersion === "number");
128
129    if (!_generatedImageCacheDatabase)
130        return;
131
132    var imageData = context.getImageData(0, 0, width, height);
133    var imageDataPixels = new Uint32Array(imageData.data.buffer);
134
135    var imageDataString = "";
136    for (var i = 0; i < imageDataPixels.length; ++i)
137        imageDataString += (i ? ":" : "") + (imageDataPixels[i] ? imageDataPixels[i].toString(36) : "");
138
139    _generatedImageCacheDatabase.transaction(function(tx) {
140        tx.executeSql("INSERT OR REPLACE INTO CachedImages (key, pixelRatio, imageVersion, formatVersion, data) VALUES (?, ?, ?, ?, ?)", [storageKey, window.devicePixelRatio, imageVersion, _imageStorageFormatVersion, imageDataString], null, _logSQLError);
141    }, _logSQLTransactionError);
142}
143
144function restoreImageFromStorage(storageKey, context, width, height, imageVersion, generateCallback)
145{
146    console.assert(storageKey);
147    console.assert(context);
148    console.assert(typeof width === "number");
149    console.assert(typeof height === "number");
150    console.assert(typeof imageVersion === "number");
151    console.assert(typeof generateCallback === "function");
152
153    if (!_generatedImageCacheDatabase) {
154        generateCallback();
155        return;
156    }
157
158    var imageInfo = _fetchedCachedImages[storageKey];
159
160    if (imageInfo) {
161        // We only want to keep the data around for the first use. These images
162        // are typically only used in one place. This keeps performance good
163        // during page load and frees memory that typically won't be reused.
164        delete _fetchedCachedImages[storageKey];
165    }
166
167    if (imageInfo && (!imageInfo.data || imageInfo.imageVersion !== imageVersion)) {
168        generateCallback();
169        return;
170    }
171
172    if (imageInfo) {
173        // Restore the image from the memory cache.
174        restoreImageData(imageInfo.data);
175    } else {
176        // Try fetching the image data from the database.
177        _generatedImageCacheDatabase.readTransaction(function(tx) {
178            tx.executeSql("SELECT data FROM CachedImages WHERE key = ? AND pixelRatio = ? AND imageVersion = ? AND formatVersion = ?", [storageKey, window.devicePixelRatio, imageVersion, _imageStorageFormatVersion], function(tx, result) {
179                if (!result.rows.length) {
180                    generateCallback();
181                    return;
182                }
183
184                console.assert(result.rows.length === 1);
185
186                restoreImageData(result.rows.item(0).data);
187            }, function(tx, error) {
188                _logSQLError(tx, error);
189
190                generateCallback();
191            });
192        }, _logSQLTransactionError);
193    }
194
195    function restoreImageData(imageDataString)
196    {
197        var imageData = context.createImageData(width, height);
198        var imageDataPixels = new Uint32Array(imageData.data.buffer);
199
200        var imageDataArray = imageDataString.split(":");
201        if (imageDataArray.length !== imageDataPixels.length) {
202            generateCallback();
203            return;
204        }
205
206        for (var i = 0; i < imageDataArray.length; ++i) {
207            var pixelString = imageDataArray[i];
208            imageDataPixels[i] = pixelString ? parseInt(pixelString, 36) : 0;
209        }
210
211        context.putImageData(imageData, 0, 0);
212    }
213}
214
215function generateColoredImage(inputImage, red, green, blue, alpha, width, height)
216{
217    console.assert(inputImage);
218
219    if (alpha === undefined)
220        alpha = 1;
221
222    if (width === undefined)
223        width = inputImage.width;
224
225    if (height === undefined)
226        height = inputImage.height;
227
228    if (inputImage instanceof HTMLCanvasElement) {
229        // The input is already a canvas, so we can use its context directly.
230        var inputContext = inputImage.getContext("2d");
231    } else {
232        console.assert(inputImage instanceof HTMLImageElement || inputImage instanceof HTMLVideoElement);
233
234        // The input is an image/video element, so we need to draw it into
235        // a canvas to get the pixel data.
236        var inputCanvas = document.createElement("canvas");
237        inputCanvas.width = width;
238        inputCanvas.height = height;
239
240        var inputContext = inputCanvas.getContext("2d");
241        inputContext.drawImage(inputImage, 0, 0, width, height);
242    }
243
244    var imageData = inputContext.getImageData(0, 0, width, height);
245    var imageDataPixels = new Uint32Array(imageData.data.buffer);
246
247    var isLittleEndian = Uint32Array.isLittleEndian();
248
249    // Loop over the image data and set the color channels while preserving the alpha.
250    for (var i = 0; i < imageDataPixels.length; ++i) {
251        if (isLittleEndian) {
252            var existingAlpha = 0xff & (imageDataPixels[i] >> 24);
253            imageDataPixels[i] = red | green << 8 | blue << 16 | (existingAlpha * alpha) << 24;
254        } else {
255            var existingAlpha = 0xff & imageDataPixels[i];
256            imageDataPixels[i] = red << 24 | green << 16 | blue << 8 | existingAlpha * alpha;
257        }
258    }
259
260    // Make a canvas that will be returned as the result.
261    var resultCanvas = document.createElement("canvas");
262    resultCanvas.width = width;
263    resultCanvas.height = height;
264
265    var resultContext = resultCanvas.getContext("2d");
266
267    resultContext.putImageData(imageData, 0, 0);
268
269    return resultCanvas;
270}
271
272function generateColoredImagesForCSS(imagePath, specifications, width, height, canvasIdentifierPrefix)
273{
274    console.assert(imagePath);
275    console.assert(specifications);
276    console.assert(typeof width === "number");
277    console.assert(typeof height === "number");
278
279    var scaleFactor = window.devicePixelRatio;
280    var scaledWidth = width * scaleFactor;
281    var scaledHeight = height * scaleFactor;
282
283    canvasIdentifierPrefix = canvasIdentifierPrefix || "";
284
285    const storageKeyPrefix = "generated-colored-image-";
286
287    var imageElement = null;
288    var pendingImageLoadCallbacks = [];
289
290    _registerGeneratedImageUpdateFunction(update);
291
292    function imageLoaded()
293    {
294        console.assert(imageElement);
295        console.assert(imageElement.complete);
296        for (var i = 0; i < pendingImageLoadCallbacks.length; ++i)
297            pendingImageLoadCallbacks[i]();
298        pendingImageLoadCallbacks = null;
299    }
300
301    function ensureImageIsLoaded(callback)
302    {
303        if (imageElement && imageElement.complete) {
304            callback();
305            return;
306        }
307
308        console.assert(pendingImageLoadCallbacks);
309        pendingImageLoadCallbacks.push(callback);
310
311        if (imageElement)
312            return;
313
314        imageElement = document.createElement("img");
315        imageElement.addEventListener("load", imageLoaded);
316        imageElement.width = width;
317        imageElement.height = height;
318        imageElement.src = imagePath;
319    }
320
321    function restoreImages()
322    {
323        for (var canvasIdentifier in specifications) {
324            // Don't restore active images yet.
325            if (canvasIdentifier.indexOf("active") !== -1)
326                continue;
327
328            var specification = specifications[canvasIdentifier];
329            restoreImage(canvasIdentifier, specification);
330        }
331
332        function restoreActiveImages()
333        {
334            for (var canvasIdentifier in specifications) {
335                // Only restore active images here.
336                if (canvasIdentifier.indexOf("active") === -1)
337                    continue;
338
339                var specification = specifications[canvasIdentifier];
340                restoreImage(canvasIdentifier, specification);
341            }
342        }
343
344        // Delay restoring the active states until later to improve the initial page load time.
345        setTimeout(restoreActiveImages, 500);
346    }
347
348    function restoreImage(canvasIdentifier, specification)
349    {
350        const storageKey = storageKeyPrefix + canvasIdentifierPrefix + canvasIdentifier;
351        const context = document.getCSSCanvasContext("2d", canvasIdentifierPrefix + canvasIdentifier, scaledWidth, scaledHeight);
352        restoreImageFromStorage(storageKey, context, scaledWidth, scaledHeight, specification.imageVersion || defaultImageVersion, function() {
353            ensureImageIsLoaded(generateImage.bind(null, canvasIdentifier, specification));
354        });
355    }
356
357    function update()
358    {
359        restoreImages();
360    }
361
362    function generateImage(canvasIdentifier, specification)
363    {
364        console.assert(specification.fillColor instanceof Array);
365        console.assert(specification.fillColor.length === 3 || specification.fillColor.length === 4);
366
367        const context = document.getCSSCanvasContext("2d", canvasIdentifierPrefix + canvasIdentifier, scaledWidth, scaledHeight);
368        context.save();
369        context.scale(scaleFactor, scaleFactor);
370
371        if (specification.shadowColor) {
372            context.shadowOffsetX = specification.shadowOffsetX || 0;
373            context.shadowOffsetY = specification.shadowOffsetY || 0;
374            context.shadowBlur = specification.shadowBlur || 0;
375
376            if (specification.shadowColor instanceof Array) {
377                if (specification.shadowColor.length === 3)
378                    context.shadowColor = "rgb(" + specification.shadowColor.join(", ") + ")";
379                else if (specification.shadowColor.length === 4)
380                    context.shadowColor = "rgba(" + specification.shadowColor.join(", ") + ")";
381            } else
382                context.shadowColor = specification.shadowColor;
383        }
384
385        var coloredImage = generateColoredImage(imageElement, specification.fillColor[0], specification.fillColor[1], specification.fillColor[2], specification.fillColor[3], scaledWidth, scaledHeight);
386        context.drawImage(coloredImage, 0, 0, width, height);
387
388        const storageKey = storageKeyPrefix + canvasIdentifierPrefix + canvasIdentifier;
389        saveImageToStorage(storageKey, context, scaledWidth, scaledHeight, specification.imageVersion || defaultImageVersion);
390        context.restore();
391    }
392}
393
394function generateEmbossedImages(src, width, height, states, canvasIdentifierCallback, ignoreCache)
395{
396    console.assert(src);
397    console.assert(typeof width === "number");
398    console.assert(typeof height === "number");
399    console.assert(states);
400    console.assert(states.Normal);
401    console.assert(states.Active);
402    console.assert(typeof canvasIdentifierCallback === "function");
403
404    var scaleFactor = window.devicePixelRatio;
405    var scaledWidth = width * scaleFactor;
406    var scaledHeight = height * scaleFactor;
407
408    const imageVersion = defaultImageVersion;
409
410    const storageKeyPrefix = "generated-embossed-image-";
411
412    var image = null;
413    var pendingImageLoadCallbacks = [];
414
415    _registerGeneratedImageUpdateFunction(update);
416
417    function imageLoaded()
418    {
419        console.assert(image);
420        console.assert(image.complete);
421        for (var i = 0; i < pendingImageLoadCallbacks.length; ++i)
422            pendingImageLoadCallbacks[i]();
423        pendingImageLoadCallbacks = null;
424    }
425
426    function ensureImageIsLoaded(callback)
427    {
428        if (image && image.complete) {
429            callback();
430            return;
431        }
432
433        console.assert(pendingImageLoadCallbacks);
434        pendingImageLoadCallbacks.push(callback);
435
436        if (image)
437            return;
438
439        image = document.createElement("img");
440        image.addEventListener("load", imageLoaded);
441        image.width = width;
442        image.height = height;
443        image.src = src;
444    }
445
446    function restoreImages()
447    {
448        restoreImage(states.Normal);
449        if (states.Focus)
450            restoreImage(states.Focus);
451
452        function restoreActiveImages()
453        {
454            restoreImage(states.Active);
455            if (states.ActiveFocus)
456                restoreImage(states.ActiveFocus);
457        }
458
459        // Delay restoring the active states until later to improve the initial page load time.
460        setTimeout(restoreActiveImages, 500);
461    }
462
463    function restoreImage(state)
464    {
465        const storageKey = storageKeyPrefix + canvasIdentifierCallback(state);
466        const context = document.getCSSCanvasContext("2d", canvasIdentifierCallback(state), scaledWidth, scaledHeight);
467        restoreImageFromStorage(storageKey, context, scaledWidth, scaledHeight, imageVersion, function() {
468            ensureImageIsLoaded(generateImage.bind(null, state));
469        });
470    }
471
472    function update()
473    {
474        if (ignoreCache)
475            generateImages();
476        else
477            restoreImages();
478    }
479
480    function generateImages()
481    {
482        ensureImageIsLoaded(generateImage.bind(null, states.Normal));
483
484        if (states.Focus)
485            ensureImageIsLoaded(generateImage.bind(null, states.Focus));
486
487        function generateActiveImages()
488        {
489            ensureImageIsLoaded(generateImage.bind(null, states.Active));
490
491            if (states.ActiveFocus)
492                ensureImageIsLoaded(generateImage.bind(null, states.ActiveFocus));
493        }
494
495        // Delay generating the active states until later to improve the initial page load time.
496        setTimeout(generateActiveImages, 500);
497    }
498
499    function generateImage(state)
500    {
501        function generateModernImage()
502        {
503            const context = document.getCSSCanvasContext("2d", canvasIdentifierCallback(state), scaledWidth, scaledHeight);
504            context.save();
505            context.scale(scaleFactor, scaleFactor);
506
507            context.clearRect(0, 0, width, height);
508
509            var gradient = context.createLinearGradient(0, 0, 0, height);
510            if (state === states.Active) {
511                gradient.addColorStop(0, "rgb(65, 65, 65)");
512                gradient.addColorStop(1, "rgb(70, 70, 70)");
513            } else if (state === states.Focus) {
514                gradient.addColorStop(0, "rgb(0, 123, 247)");
515                gradient.addColorStop(1, "rgb(0, 128, 252)");
516            } else if (state === states.ActiveFocus) {
517                gradient.addColorStop(0, "rgb(0, 62, 210)");
518                gradient.addColorStop(1, "rgb(0, 67, 215)");
519            } else {
520                gradient.addColorStop(0, "rgb(75, 75, 75)");
521                gradient.addColorStop(1, "rgb(80, 80, 80)");
522            }
523
524            context.fillStyle = gradient;
525            context.fillRect(0, 0, width, height);
526
527            // Apply the mask to keep just the inner shape of the glyph.
528            _applyImageMask(context, image);
529
530            if (!ignoreCache) {
531                const storageKey = storageKeyPrefix + canvasIdentifierCallback(state);
532                saveImageToStorage(storageKey, context, scaledWidth, scaledHeight, imageVersion);
533            }
534
535            context.restore();
536        }
537
538        function generateLegacyImage()
539        {
540            const depth = 1 * scaleFactor;
541            const shadowDepth = depth;
542            const shadowBlur = depth - 1;
543            const glowBlur = 2;
544
545            const context = document.getCSSCanvasContext("2d", canvasIdentifierCallback(state), scaledWidth, scaledHeight);
546            context.save();
547            context.scale(scaleFactor, scaleFactor);
548
549            context.clearRect(0, 0, width, height);
550
551            if (depth > 0) {
552                // Use scratch canvas so we can apply the draw the white drop shadow
553                // to the whole glyph at the end.
554
555                var scratchCanvas = document.createElement("canvas");
556                scratchCanvas.width = scaledWidth;
557                scratchCanvas.height = scaledHeight;
558
559                var scratchContext = scratchCanvas.getContext("2d");
560                scratchContext.scale(scaleFactor, scaleFactor);
561            } else
562                var scratchContext = context;
563
564            var gradient = scratchContext.createLinearGradient(0, 0, 0, height);
565            if (state === states.Active) {
566                gradient.addColorStop(0, "rgb(60, 60, 60)");
567                gradient.addColorStop(1, "rgb(100, 100, 100)");
568            } else if (state === states.Focus) {
569                gradient.addColorStop(0, "rgb(50, 135, 200)");
570                gradient.addColorStop(1, "rgb(60, 155, 225)");
571            } else if (state === states.ActiveFocus) {
572                gradient.addColorStop(0, "rgb(30, 115, 185)");
573                gradient.addColorStop(1, "rgb(40, 135, 200)");
574            } else {
575                gradient.addColorStop(0, "rgb(90, 90, 90)");
576                gradient.addColorStop(1, "rgb(145, 145, 145)");
577            }
578
579            scratchContext.fillStyle = gradient;
580            scratchContext.fillRect(0, 0, width, height);
581
582            if (depth > 0) {
583                // Invert the image to use as a reverse image mask for the inner shadows.
584                // Pass in the color to use for the opaque areas to prevent "black halos"
585                // later when applying the final image mask.
586
587                if (state === states.Active)
588                    var invertedImage = _invertMaskImage(image, 60, 60, 60);
589                else if (state === states.Focus)
590                    var invertedImage = _invertMaskImage(image, 45, 145, 210);
591                else if (state === states.ActiveFocus)
592                    var invertedImage = _invertMaskImage(image, 35, 125, 195);
593                else
594                    var invertedImage = _invertMaskImage(image, 90, 90, 90);
595
596                if (state === states.Focus) {
597                    // Double draw the blurry inner shadow to get the right effect.
598                    _drawImageShadow(scratchContext, 0, 0, shadowDepth, "rgb(10, 95, 150)", invertedImage);
599                    _drawImageShadow(scratchContext, 0, 0, shadowDepth, "rgb(10, 95, 150)", invertedImage);
600
601                    // Draw the inner shadow.
602                    _drawImageShadow(scratchContext, 0, shadowDepth, shadowBlur, "rgb(0, 80, 170)", invertedImage);
603                } else if (state === states.ActiveFocus) {
604                    // Double draw the blurry inner shadow to get the right effect.
605                    _drawImageShadow(scratchContext, 0, 0, shadowDepth, "rgb(0, 80, 100)", invertedImage);
606                    _drawImageShadow(scratchContext, 0, 0, shadowDepth, "rgb(0, 80, 100)", invertedImage);
607
608                    // Draw the inner shadow.
609                    _drawImageShadow(scratchContext, 0, shadowDepth, shadowBlur, "rgb(0, 65, 150)", invertedImage);
610                } else {
611                    // Double draw the blurry inner shadow to get the right effect.
612                    _drawImageShadow(scratchContext, 0, 0, shadowDepth, "rgba(0, 0, 0, 1)", invertedImage);
613                    _drawImageShadow(scratchContext, 0, 0, shadowDepth, "rgba(0, 0, 0, 1)", invertedImage);
614
615                    // Draw the inner shadow.
616                    _drawImageShadow(scratchContext, 0, shadowDepth, shadowBlur, "rgba(0, 0, 0, 0.6)", invertedImage);
617                }
618            }
619
620            // Apply the mask to keep just the inner shape of the glyph.
621            _applyImageMask(scratchContext, image);
622
623            // Draw the white drop shadow.
624            if (depth > 0)
625                _drawImageShadow(context, 0, shadowDepth, shadowBlur, "rgba(255, 255, 255, 0.6)", scratchCanvas);
626
627            // Draw a subtle glow for the focus states.
628            if (state === states.Focus || state === states.ActiveFocus)
629                _drawImageShadow(context, 0, 0, glowBlur, "rgba(20, 100, 220, 0.4)", scratchCanvas);
630
631            if (!ignoreCache) {
632                const storageKey = storageKeyPrefix + canvasIdentifierCallback(state);
633                saveImageToStorage(storageKey, context, scaledWidth, scaledHeight, imageVersion);
634            }
635
636            context.restore();
637        }
638
639        if (WebInspector.Platform.isLegacyMacOS)
640            generateLegacyImage();
641        else
642            generateModernImage();
643    }
644
645    function _drawImageShadow(context, xOffset, yOffset, blur, color, image) {
646        context.save();
647
648        context.shadowOffsetX = xOffset || 0;
649        context.shadowOffsetY = yOffset || 0;
650        context.shadowBlur = blur || 0;
651        context.shadowColor = color || "black";
652
653        context.drawImage(image, 0, 0, width, height);
654
655        context.restore();
656    }
657
658    function _invertMaskImage(image, red, green, blue) {
659        var bufferCanvas = document.createElement("canvas");
660        bufferCanvas.width = scaledWidth;
661        bufferCanvas.height = scaledHeight;
662
663        var buffer = bufferCanvas.getContext("2d");
664        buffer.scale(scaleFactor, scaleFactor);
665        buffer.drawImage(image, 0, 0, width, height);
666
667        var imageData = buffer.getImageData(0, 0, scaledWidth, scaledHeight);
668        var imageDataPixels = new Uint32Array(imageData.data.buffer);
669
670        red = red || 0;
671        green = green || 0;
672        blue = blue || 0;
673
674        var isLittleEndian = Uint32Array.isLittleEndian();
675
676        for (var i = 0; i < imageDataPixels.length; ++i) {
677            if (isLittleEndian) {
678                var existingAlpha = 0xff & (imageDataPixels[i] >> 24);
679                imageDataPixels[i] = red | green << 8 | blue << 16 | (255 - existingAlpha) << 24;
680            } else {
681                var existingAlpha = 0xff & imageDataPixels[i];
682                imageDataPixels[i] = red << 24 | green << 16 | blue << 8 | 255 - existingAlpha;
683            }
684        }
685
686        buffer.putImageData(imageData, 0, 0);
687
688        return bufferCanvas;
689    }
690
691    function _applyImageMask(context, image) {
692        var maskCanvas = document.createElement("canvas");
693        maskCanvas.width = scaledWidth;
694        maskCanvas.height = scaledHeight;
695
696        var mask = maskCanvas.getContext("2d");
697        mask.scale(scaleFactor, scaleFactor);
698        mask.drawImage(image, 0, 0, width, height);
699
700        var imageData = context.getImageData(0, 0, scaledWidth, scaledHeight);
701        var imageDataPixels = imageData.data;
702
703        var maskImageDataPixels = mask.getImageData(0, 0, scaledWidth, scaledHeight).data;
704
705        for (var i = 3; i < imageDataPixels.length; i += 4)
706            imageDataPixels[i] = maskImageDataPixels[i] * (imageDataPixels[i] / 255);
707
708        context.putImageData(imageData, 0, 0);
709    }
710}
711
712var svgImageCache = {};
713
714function loadSVGImageDocumentElement(url, callback)
715{
716    function invokeCallbackWithDocument(svgText) {
717        var parser = new DOMParser;
718        var doc = parser.parseFromString(svgText, "image/svg+xml");
719        callback(doc.documentElement);
720    }
721
722    function imageLoad(event) {
723        if (xhr.status === 0 || xhr.status === 200) {
724            var svgText = xhr.responseText;
725            svgImageCache[url] = svgText;
726            invokeCallbackWithDocument(svgText);
727        } else {
728            console.error("Unexpected XHR status (" + xhr.status + ") loading SVG image: " + url);
729            callback(null);
730        }
731    }
732
733    function imageError(event) {
734        console.error("Unexpected failure loading SVG image: " + url);
735        callback(null);
736    }
737
738    var cachedSVGText = svgImageCache[url];
739    if (cachedSVGText) {
740        invokeCallbackWithDocument(cachedSVGText);
741        return;
742    }
743
744    var xhr = new XMLHttpRequest;
745    xhr.open("GET", url, true);
746    xhr.addEventListener("load", imageLoad);
747    xhr.addEventListener("error", imageError);
748    xhr.send();
749}
750
751function wrappedSVGDocument(url, className, title, callback)
752{
753    loadSVGImageDocumentElement(url, function(svgDocument) {
754        if (!svgDocument) {
755            callback(null);
756            return;
757        }
758
759        var wrapper = document.createElement("div");
760        if (className)
761            wrapper.className = className;
762        if (title)
763            wrapper.title = title;
764        wrapper.appendChild(svgDocument);
765
766        callback(wrapper);
767    });
768}
769