├── .babelrc ├── .gitignore ├── index.js ├── .travis.yml ├── examples ├── img │ ├── bear.jpg │ ├── boat.jpg │ ├── logo.png │ ├── wolf.jpg │ ├── coffee.jpg │ ├── field.jpg │ ├── forest.jpg │ ├── peridot.png │ └── shepherd.jpg ├── css │ ├── purple.jpg │ ├── img │ │ └── purple.jpg │ ├── style.css │ └── bootstrap-theme.min.css ├── fonts │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.ttf │ ├── glyphicons-halflings-regular.woff │ └── glyphicons-halflings-regular.woff2 ├── pooling.html ├── index.html ├── scripts │ ├── watermark.min.js │ ├── upload.js │ └── watermark.js ├── uploading.html ├── images.html ├── text.html └── docs.html ├── jestEnvironment.js ├── lib ├── canvas │ ├── index.js │ ├── pool.js │ └── __tests__ │ │ └── pool-test.js ├── functions │ ├── __tests__ │ │ └── functions-test.js │ └── index.js ├── object │ ├── index.js │ └── __tests__ │ │ └── object-test.js ├── image │ ├── __tests__ │ │ └── image-test.js │ └── index.js ├── style │ ├── index.js │ ├── image │ │ └── index.js │ └── text │ │ └── index.js ├── blob │ ├── __tests__ │ │ └── blob-test.js │ └── index.js ├── __tests__ │ └── watermark-test.js └── index.js ├── CHANGELOG.md ├── bower.json ├── LICENSE ├── webpack.config.js ├── package.json ├── README.md └── dist ├── watermark.min.js └── watermark.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/index').default; 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "12.13.0" 5 | -------------------------------------------------------------------------------- /examples/img/bear.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brianium/watermarkjs/HEAD/examples/img/bear.jpg -------------------------------------------------------------------------------- /examples/img/boat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brianium/watermarkjs/HEAD/examples/img/boat.jpg -------------------------------------------------------------------------------- /examples/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brianium/watermarkjs/HEAD/examples/img/logo.png -------------------------------------------------------------------------------- /examples/img/wolf.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brianium/watermarkjs/HEAD/examples/img/wolf.jpg -------------------------------------------------------------------------------- /examples/css/purple.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brianium/watermarkjs/HEAD/examples/css/purple.jpg -------------------------------------------------------------------------------- /examples/img/coffee.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brianium/watermarkjs/HEAD/examples/img/coffee.jpg -------------------------------------------------------------------------------- /examples/img/field.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brianium/watermarkjs/HEAD/examples/img/field.jpg -------------------------------------------------------------------------------- /examples/img/forest.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brianium/watermarkjs/HEAD/examples/img/forest.jpg -------------------------------------------------------------------------------- /examples/img/peridot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brianium/watermarkjs/HEAD/examples/img/peridot.png -------------------------------------------------------------------------------- /examples/img/shepherd.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brianium/watermarkjs/HEAD/examples/img/shepherd.jpg -------------------------------------------------------------------------------- /examples/css/img/purple.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brianium/watermarkjs/HEAD/examples/css/img/purple.jpg -------------------------------------------------------------------------------- /examples/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brianium/watermarkjs/HEAD/examples/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /examples/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brianium/watermarkjs/HEAD/examples/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /examples/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brianium/watermarkjs/HEAD/examples/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /examples/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brianium/watermarkjs/HEAD/examples/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /jestEnvironment.js: -------------------------------------------------------------------------------- 1 | jest.autoMockOff(); 2 | 3 | window.atob = function(base64) { 4 | return 'decoded!'; 5 | } 6 | 7 | window.File = function() { 8 | 9 | }; 10 | 11 | window.Image = function() { 12 | this.width = 50; 13 | this.height = 50; 14 | } 15 | -------------------------------------------------------------------------------- /lib/canvas/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Get the data url of a canvas 3 | * 4 | * @param {HTMLCanvasElement} 5 | * @param {Paramters} Specifications according to HTMLCanvasElement.toDataURL() Documentation 6 | * @return {String} 7 | */ 8 | export function dataUrl(canvas, parameters = { type:'image/png', encoderOptions:0.92 }) { 9 | return canvas.toDataURL(parameters.type, parameters.encoderOptions); 10 | } 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | #### 2.1.0-rc.1 4 | 5 | * Updated build process to webpack 4 6 | * Updated build process to Babel 7 7 | * Updated build process to use uglify webpack plugin over cli 8 | * Updated unit test to Jest 24 9 | 10 | #### 1.1.0 11 | 12 | * Support UMD 13 | * Use Babel 6 and replace let with const where possible 14 | * Remove bundled ES6 polyfill (include it from elsewhere if you need it) 15 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "watermarkjs", 3 | "main": "dist/watermark.js", 4 | "version": "2.1.0-rc.1", 5 | "homepage": "http://brianium.github.io/watermarkjs/", 6 | "authors": [ 7 | "Brian Scaturro " 8 | ], 9 | "description": "Watermarked images in the browser", 10 | "moduleType": [ 11 | "globals" 12 | ], 13 | "keywords": [ 14 | "file", 15 | "watermark", 16 | "image", 17 | "canvas" 18 | ], 19 | "license": "MIT" 20 | } 21 | -------------------------------------------------------------------------------- /lib/functions/__tests__/functions-test.js: -------------------------------------------------------------------------------- 1 | import {sequence, identity} from '../'; 2 | 3 | describe('functions', function () { 4 | it('can create a sequence of functions', function () { 5 | const fn = sequence( 6 | (x) => x + 4, 7 | (z) => z + 2 8 | ); 9 | const value = fn(3); 10 | expect(value).toBe(9); 11 | }); 12 | 13 | it('can return a given argument', function () { 14 | const value = identity(4); 15 | expect(value).toBe(4); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /lib/object/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Extend one object with the properties of another 3 | * 4 | * @param {Object} first 5 | * @param {Object} second 6 | * @return {Object} 7 | */ 8 | export function extend(first, second) { 9 | const secondKeys = Object.keys(second); 10 | secondKeys.forEach(key => first[key] = second[key]); 11 | return first; 12 | } 13 | 14 | /** 15 | * Create a shallow copy of the object 16 | * 17 | * @param {Object} obj 18 | * @return {Object} 19 | */ 20 | export function clone(obj) { 21 | return extend({}, obj); 22 | } 23 | -------------------------------------------------------------------------------- /lib/functions/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Return a function that executes a sequence of functions from left to right, 3 | * passing the result of a previous operation to the next 4 | * 5 | * @param {...funcs} 6 | * @return {Function} 7 | */ 8 | export function sequence(...funcs) { 9 | return function(value) { 10 | return funcs.reduce((val, fn) => fn.call(null, val), value); 11 | } 12 | } 13 | 14 | /** 15 | * Return the argument passed to it 16 | * 17 | * @param {Mixed} x 18 | * @return {Mixed} 19 | */ 20 | export function identity(x) { 21 | return x; 22 | } 23 | -------------------------------------------------------------------------------- /lib/image/__tests__/image-test.js: -------------------------------------------------------------------------------- 1 | import {createImage, getLoader} from '../'; 2 | 3 | describe('image', function () { 4 | it('can create a new image', function () { 5 | const onload = () => console.log('loaded'); 6 | const img = createImage('url1', onload); 7 | expect(img.src).toBe('url1'); 8 | expect(img.onload).toBe(onload); 9 | }); 10 | 11 | it('returns an identity loader function when given an image', function () { 12 | const img = new Image(); 13 | const loader = getLoader(img); 14 | expect(loader(img)).toBe(img); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /lib/object/__tests__/object-test.js: -------------------------------------------------------------------------------- 1 | import {extend, clone} from '../'; 2 | 3 | describe('object', function(){ 4 | describe('extend()', function() { 5 | it('can extend first object with second object', function(){ 6 | let first = {foo: 'bar', baz: 'hash'}, 7 | second = {baz: 'dash', joe: 'bob'}; 8 | 9 | let merged = extend(first, second); 10 | expect(merged).toEqual({foo: 'bar', baz: 'dash', joe: 'bob'}); 11 | }); 12 | }); 13 | 14 | describe('clone()', function() { 15 | it('should create a shallow copy of an object', () => { 16 | const obj = {'name': 'Brian'}; 17 | 18 | const copy = clone(obj); 19 | 20 | expect(obj).not.toBe(copy); 21 | expect(obj).toEqual(copy); 22 | }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /lib/style/index.js: -------------------------------------------------------------------------------- 1 | import * as img from './image'; 2 | import * as txt from './text'; 3 | 4 | /** 5 | * @typedef {Object} DrawResult 6 | * @property {HTMLCanvasElement} canvas - the end result of a draw 7 | * @property {HTMLCanvasElement[]} sources - the sources used in the draw 8 | */ 9 | 10 | export const image = img; 11 | export const text = txt; 12 | 13 | /** 14 | * Create a DrawResult by apply a list of canvas elements to a draw function 15 | * 16 | * @param {Function} draw - the draw function used to create a DrawResult 17 | * @param {HTMLCanvasElement} sources - the canvases used by the draw function 18 | * @return {DrawResult} 19 | */ 20 | export function result(draw, sources) { 21 | const canvas = draw.apply(null, sources); 22 | return { 23 | canvas, 24 | sources 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /lib/blob/__tests__/blob-test.js: -------------------------------------------------------------------------------- 1 | import {split, decode, uint8} from '../'; 2 | 3 | const testUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJYAAACWCAYAAAA8AXHiAAAgAElEQ…z4wYdQblhRRGYSxWfKk909djpT+VOeAue/QVI/7VHKn/3/ANXZ6cTaQP0eAAAAAElFTkSuQmCC'; 4 | 5 | describe('blobs', function () { 6 | it('can break a data url into parts', function () { 7 | const parts = split(testUrl); 8 | expect(parts[0]).toEqual('image/png'); 9 | expect(parts[1][0]).toEqual('i'); 10 | }); 11 | 12 | it('can decode base64 content', function () { 13 | const parts = split(testUrl); 14 | const decoded = decode(parts[1]); 15 | expect(decoded).toEqual(atob(parts[1])); 16 | }); 17 | 18 | it('can convert raw data to a Uint8Array', function () { 19 | const decoded = decode('hello'); 20 | const ints = uint8(decoded); 21 | expect(ints[0]).toEqual(decoded.charCodeAt(0)); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This software is released under the MIT license: 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /lib/blob/index.js: -------------------------------------------------------------------------------- 1 | import {sequence} from '../functions'; 2 | 3 | const url = /^data:([^;]+);base64,(.*)$/; 4 | 5 | /** 6 | * Split a data url into a content type and raw data 7 | * 8 | * @param {String} dataUrl 9 | * @return {Array} 10 | */ 11 | export function split(dataUrl) { 12 | return url 13 | .exec(dataUrl) 14 | .slice(1); 15 | } 16 | 17 | /** 18 | * Decode a base64 string 19 | * 20 | * @param {String} base64 21 | * @return {String} 22 | */ 23 | export function decode(base64) { 24 | return window.atob(base64); 25 | } 26 | 27 | /** 28 | * Return a string of raw data as a Uint8Array 29 | * 30 | * @param {String} data 31 | * @return {UInt8Array} 32 | */ 33 | export function uint8(data) { 34 | const length = data.length; 35 | const uints = new Uint8Array(length); 36 | 37 | for (let i = 0; i < length; i++) { 38 | uints[i] = data.charCodeAt(i); 39 | } 40 | 41 | return uints; 42 | } 43 | 44 | /** 45 | * Turns a data url into a blob object 46 | * 47 | * @param {String} dataUrl 48 | * @return {Blob} 49 | */ 50 | export const blob = sequence( 51 | split, 52 | parts => [decode(parts[1]), parts[0]], 53 | blob => new Blob([uint8(blob[0])], {type: blob[1]}) 54 | ); 55 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); 3 | 4 | module.exports = [{ 5 | entry: "./index", 6 | output: { 7 | path: path.resolve(__dirname, "dist"), 8 | filename: "watermark.js", 9 | library: "watermark", 10 | libraryTarget: "umd" 11 | }, 12 | optimization: { 13 | minimize: false 14 | }, 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.js$/, 19 | loader: 'babel-loader', 20 | options: { 21 | presets: ['@babel/preset-env'] 22 | }, 23 | include: [ 24 | path.resolve(__dirname, "lib") 25 | ] 26 | } 27 | ] 28 | } 29 | },{ 30 | entry: "./index", 31 | output: { 32 | path: path.resolve(__dirname, "dist"), 33 | filename: "watermark.min.js", 34 | library: "watermark", 35 | libraryTarget: "umd" 36 | }, 37 | optimization: { 38 | minimizer: [ 39 | new UglifyJsPlugin() 40 | ] 41 | }, 42 | module: { 43 | rules: [ 44 | { 45 | test: /\.js$/, 46 | loader: 'babel-loader', 47 | options: { 48 | presets: ['@babel/preset-env'] 49 | }, 50 | include: [ 51 | path.resolve(__dirname, "lib") 52 | ] 53 | } 54 | ] 55 | } 56 | }] 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "watermarkjs", 3 | "version": "2.1.1", 4 | "description": "Watermarked images in the browser", 5 | "main": "dist/watermark.js", 6 | "scripts": { 7 | "test": "jest", 8 | "bundle": "webpack", 9 | "sync": "browser-sync start --server examples --index index.html --files 'examples/**/*.css, examples/**/*.html, examples/**/*.js' --port 4000", 10 | "copy-dist": "cp dist/* examples/scripts", 11 | "watch": "watch \"npm run build\" lib", 12 | "build": "npm run bundle && npm run copy-dist && notify -t 'npm run build' -m 'complete'", 13 | "dev": "npm run watch & npm run sync" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git@github.com:brianium/watermarkjs.git" 18 | }, 19 | "keywords": [ 20 | "canvas", 21 | "watermark", 22 | "image", 23 | "file" 24 | ], 25 | "author": "Brian Scaturro ", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/brianium/watermarkjs/issues" 29 | }, 30 | "homepage": "https://github.com/brianium/watermarkjs", 31 | "devDependencies": { 32 | "@babel/core": "^7.7.2", 33 | "@babel/preset-env": "^7.7.1", 34 | "babel-jest": "^24.9.0", 35 | "babel-loader": "^8.0.6", 36 | "babelify": "^10.0.0", 37 | "browser-sync": "^2.26.7", 38 | "browserify": "^16.5.0", 39 | "browserify-derequire": "^1.0.1", 40 | "jest-cli": "^24.9.0", 41 | "node-notifier-cli": "^1.1.2", 42 | "uglifyjs-webpack-plugin": "^2.2.0", 43 | "watch": "^1.0.2", 44 | "webpack": "^4.41.2", 45 | "webpack-cli": "^3.3.10" 46 | }, 47 | "jest": { 48 | "setupFiles": [ 49 | "/jestEnvironment.js" 50 | ], 51 | "transform": { 52 | "\\.js$": [ 53 | "babel-jest" 54 | ] 55 | }, 56 | "moduleFileExtensions": [ 57 | "js" 58 | ] 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/canvas/pool.js: -------------------------------------------------------------------------------- 1 | /** 2 | * An immutable canvas pool allowing more efficient use of canvas resources 3 | * 4 | * @typedef {Object} CanvasPool 5 | * @property {Function} pop - return a promise that will evaluate to a canvas 6 | * @property {Number} length - the number of available canvas elements 7 | * @property {HTMLCanvasElement[]} elements - the canvas elements used by the pool 8 | * @property {Function} clear - empty the pool of canvas elements 9 | * @property {Function} release - free a pool up for release and return the data url 10 | */ 11 | 12 | /** 13 | * Create a CanvasPool with the given size 14 | * 15 | * @param {Number} size 16 | * @param {HTMLCanvasElement[]} elements 17 | * @param {EventEmitter} eventEmitter 18 | * @return {CanvasPool} 19 | */ 20 | export function CanvasPool() { 21 | const canvases = []; 22 | 23 | return { 24 | /** 25 | * Get the next available canvas from the pool 26 | * 27 | * @return {HTMLCanvasElement} 28 | */ 29 | pop() { 30 | if (this.length === 0) { 31 | canvases.push(document.createElement('canvas')); 32 | } 33 | 34 | return canvases.pop(); 35 | }, 36 | 37 | /** 38 | * Return the number of available canvas elements in the pool 39 | * 40 | * @return {Number} 41 | */ 42 | get length() { 43 | return canvases.length; 44 | }, 45 | 46 | /** 47 | * Return a canvas to the pool. This function will clear the canvas for reuse 48 | * 49 | * @param {HTMLCanvasElement} canvas 50 | * @return {String} 51 | */ 52 | release(canvas) { 53 | const context = canvas.getContext('2d'); 54 | context.clearRect(0, 0, canvas.width, canvas.height); 55 | canvases.push(canvas); 56 | }, 57 | 58 | /** 59 | * Empty the pool, destroying any references to canvas objects 60 | */ 61 | clear() { 62 | canvases.splice(0, canvases.length); 63 | }, 64 | 65 | /** 66 | * Return the collection of canvases in the pool 67 | * 68 | * @return {HTMLCanvasElement[]} 69 | */ 70 | get elements() { 71 | return canvases; 72 | } 73 | } 74 | } 75 | 76 | const shared = CanvasPool(); 77 | export default shared; 78 | -------------------------------------------------------------------------------- /lib/canvas/__tests__/pool-test.js: -------------------------------------------------------------------------------- 1 | import { CanvasPool } from '../pool'; 2 | 3 | /** 4 | * Mock the clearRect method of a canvas 5 | * 6 | * @param {HTMLCanvasElement} canvas 7 | * @return {HTMLCanvasElement} 8 | */ 9 | const releasable = canvas => { 10 | const context = { 11 | clearRect: jest.fn() 12 | }; 13 | canvas.getContext = twoD => { 14 | canvas.context = context; 15 | return context; 16 | } 17 | return canvas; 18 | }; 19 | 20 | /** 21 | * Fill the pool with the given number of canvas elements 22 | * 23 | * @param {CanvasPool} pool 24 | * @param {Number} length 25 | */ 26 | function fill(pool, length) { 27 | let i = 0; 28 | const canvases = []; 29 | while (i < length) { 30 | canvases.push(releasable(pool.pop())); 31 | i++; 32 | } 33 | 34 | canvases.forEach(pool.release); 35 | } 36 | 37 | describe('CanvasPool', () => { 38 | let pool; 39 | 40 | beforeEach(() => { 41 | pool = CanvasPool(); 42 | }); 43 | 44 | describe('pop()', () => { 45 | it('should return a canvas', () => { 46 | const canvas = pool.pop() 47 | expect(canvas).toBeTruthy(); 48 | }); 49 | 50 | it('should reuse a canvas if available', () => { 51 | const canvas = pool.pop(); 52 | 53 | // free up canvas 54 | pool.release(releasable(canvas)); 55 | 56 | const other = pool.pop(); 57 | 58 | expect(other).toBe(canvas); 59 | }); 60 | }); 61 | 62 | describe('.length', () => { 63 | it('should return the number of available canvas elements', () => { 64 | fill(pool, 5); 65 | expect(pool.length).toBe(5); 66 | }); 67 | }); 68 | 69 | describe('release()', () => { 70 | it('should wipe the canvas to make it ready for other draws', () => { 71 | const canvas = pool.pop() 72 | releasable(canvas).width = 50; 73 | canvas.height = 60; 74 | pool.release(canvas); 75 | expect(canvas.context.clearRect).toBeCalledWith(0, 0, 50, 60); 76 | }); 77 | 78 | it('should increase the length of available canvas elements', () => { 79 | const canvas = pool.pop(); 80 | pool.release(releasable(canvas)); 81 | expect(pool.length).toBe(1); 82 | }); 83 | }); 84 | 85 | describe('clear()', () => { 86 | it('should wipe any canvas references', () => { 87 | fill(pool, 5); 88 | pool.clear(); 89 | expect(pool.length).toBe(0); 90 | }); 91 | }); 92 | }); 93 | -------------------------------------------------------------------------------- /lib/__tests__/watermark-test.js: -------------------------------------------------------------------------------- 1 | import watermark from '../'; 2 | 3 | const blob = require('../blob'); 4 | blob.blob = jest.fn(); 5 | 6 | const canvas = require('../canvas'); 7 | canvas.dataUrl = jest.fn(); 8 | 9 | const image = require('../image'); 10 | image.load = jest.fn(); 11 | image.mapToCanvas = jest.fn(); 12 | image.imageToCanvas = jest.fn(); 13 | image.createImage = jest.fn(); 14 | 15 | const style = require('../style'); 16 | style.result = jest.fn(); 17 | 18 | describe('watermark', function () { 19 | 20 | let promise = { 21 | then(fn) { 22 | fn(); 23 | return this; 24 | } 25 | }; 26 | 27 | beforeEach(function () { 28 | image.load.mockReturnValueOnce(promise); 29 | }); 30 | 31 | it('can load urls and files', function () { 32 | const urls = ['url1', new File()]; 33 | const init = () => console.log(initialized); 34 | 35 | watermark(urls, { 36 | init 37 | }); 38 | 39 | expect(image.load).toBeCalledWith(urls, init); 40 | }); 41 | 42 | describe('.dataUrl()', function () { 43 | it('returns a new object structure', function () { 44 | const draw = jest.fn(); 45 | const first = watermark(['url1', 'url2'], {}, promise); 46 | first.then = () => first; 47 | const second = first.dataUrl(draw); 48 | 49 | expect(first).not.toBe(second); 50 | }); 51 | }); 52 | 53 | describe('.blob()', function () { 54 | it('should delegate to the dataUrl function and map to a blob', function () { 55 | const mark = watermark(['url1', 'url1'], {}, promise); 56 | mark.dataUrl = jest.fn(); 57 | mark.dataUrl.mockReturnValueOnce(promise); 58 | const draw = jest.fn(); 59 | 60 | const newMark = mark.blob(draw); 61 | 62 | expect(mark.dataUrl).toBeCalledWith(draw); 63 | expect(blob.blob).toBeCalled(); 64 | expect(newMark).not.toBe(mark); 65 | }); 66 | }); 67 | 68 | describe('.image()', function () { 69 | it('should delegate to the dataUrl function and map to an image', function () { 70 | const mark = watermark(['url1', 'url1'], {}, promise); 71 | mark.dataUrl = jest.fn(); 72 | mark.dataUrl.mockReturnValueOnce(promise); 73 | const draw = jest.fn(); 74 | 75 | const newMark = mark.image(draw); 76 | 77 | expect(mark.dataUrl).toBeCalledWith(draw); 78 | expect(image.createImage).toBeCalled(); 79 | expect(newMark).not.toBe(mark); 80 | }); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /lib/style/image/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Return a function for positioning a watermark on a target canvas 3 | * 4 | * @param {Function} xFn - a function to determine an x value 5 | * @param {Function} yFn - a function to determine a y value 6 | * @param {Number} alpha 7 | * @return {Function} 8 | */ 9 | export function atPos(xFn, yFn, alpha) { 10 | alpha || (alpha = 1.0); 11 | return function (target, watermark) { 12 | const context = target.getContext('2d'); 13 | context.save(); 14 | 15 | context.globalAlpha = alpha; 16 | context.drawImage(watermark, xFn(target, watermark), yFn(target, watermark)); 17 | 18 | context.restore(); 19 | return target; 20 | } 21 | } 22 | 23 | 24 | /** 25 | * Place the watermark in the lower right corner of the target 26 | * image 27 | * 28 | * @param {Number} alpha 29 | * @return {Function} 30 | */ 31 | export function lowerRight(alpha) { 32 | return atPos( 33 | (target, mark) => target.width - (mark.width + 10), 34 | (target, mark) => target.height - (mark.height + 10), 35 | alpha 36 | ); 37 | } 38 | 39 | /** 40 | * Place the watermark in the upper right corner of the target 41 | * image 42 | * 43 | * @param {Number} alpha 44 | * @return {Function} 45 | */ 46 | export function upperRight(alpha) { 47 | return atPos( 48 | (target, mark) => target.width - (mark.width + 10), 49 | (target, mark) => 10, 50 | alpha 51 | ); 52 | } 53 | 54 | /** 55 | * Place the watermark in the lower left corner of the target 56 | * image 57 | * 58 | * @param {Number} alpha 59 | * @return {Function} 60 | */ 61 | export function lowerLeft(alpha) { 62 | return atPos( 63 | (target, mark) => 10, 64 | (target, mark) => target.height - (mark.height + 10), 65 | alpha 66 | ); 67 | } 68 | 69 | /** 70 | * Place the watermark in the upper left corner of the target 71 | * image 72 | * 73 | * @param {Number} alpha 74 | * @return {Function} 75 | */ 76 | export function upperLeft(alpha) { 77 | return atPos( 78 | (target, mark) => 10, 79 | (target, mark) => 10, 80 | alpha 81 | ); 82 | } 83 | 84 | /** 85 | * Place the watermark in the center of the target 86 | * image 87 | * 88 | * @param {Number} alpha 89 | * @return {Function} 90 | */ 91 | export function center(alpha) { 92 | return atPos( 93 | (target, mark) => (target.width - mark.width) / 2, 94 | (target, mark) => (target.height - mark.height) / 2, 95 | alpha 96 | ); 97 | } 98 | -------------------------------------------------------------------------------- /examples/css/style.css: -------------------------------------------------------------------------------- 1 | a:link, 2 | a:visited { 3 | color: #fff; 4 | text-decoration: underline; 5 | } 6 | 7 | a:hover, 8 | a:active { 9 | color: #A974C5; 10 | } 11 | 12 | html, 13 | body { 14 | background: url('img/purple.jpg') repeat left top; 15 | font-family: 'Maven Pro', sans-serif; 16 | } 17 | 18 | h1 { 19 | font-size: 72px; 20 | } 21 | 22 | h1 a:link, 23 | h1 a:visited { 24 | text-decoration: none; 25 | } 26 | 27 | h1, h2, h3, h4, h5, h6 { 28 | color: #fff; 29 | font-family: 'Josefin Slab', serif; 30 | text-shadow: 0 -1px #000; 31 | } 32 | 33 | img { 34 | border: 0; 35 | } 36 | 37 | nav { 38 | margin-bottom: 40px; 39 | margin-top: -20px; 40 | position:relative; 41 | right: 20px; 42 | text-align: center; 43 | } 44 | 45 | nav ul { 46 | list-style: none; 47 | } 48 | 49 | nav li { 50 | display: inline-block; 51 | margin-right: 10px; 52 | } 53 | 54 | nav strong { 55 | color: #fff; 56 | font-size: 22px; 57 | } 58 | 59 | nav a { 60 | font-size: 22px; 61 | } 62 | 63 | p { 64 | color: #fff; 65 | font-size: 22px; 66 | } 67 | 68 | pre { 69 | background: transparent; 70 | border: 0; 71 | margin: 0 auto; 72 | padding: 0; 73 | text-align: left; 74 | } 75 | 76 | .lead { 77 | margin-bottom: 40px; 78 | } 79 | 80 | .separator { 81 | color: #fff; 82 | } 83 | 84 | .example { 85 | text-align: center; 86 | } 87 | 88 | 89 | .example pre { 90 | width: 600px; 91 | } 92 | 93 | .example .hljs { 94 | border-radius: 4px; 95 | } 96 | 97 | .example img, 98 | #preview img { 99 | border: 10px solid #fff; 100 | box-shadow: 3px 6px 5px rgba(0, 0, 0, 0.3); 101 | max-width: 100%; 102 | } 103 | 104 | .btn-file { 105 | position: relative; 106 | overflow: hidden; 107 | } 108 | 109 | .btn-file input[type=file] { 110 | position: absolute; 111 | top: 0; 112 | right: 0; 113 | min-width: 100%; 114 | min-height: 100%; 115 | 116 | font-size: 100px; 117 | text-align: right; 118 | filter: alpha(opacity=0); 119 | opacity: 0; 120 | outline: none; 121 | background: white; 122 | cursor: inherit; 123 | display: block; 124 | } 125 | 126 | .help-block { 127 | color: #fff; 128 | margin-bottom: 25px; 129 | } 130 | 131 | #preview { 132 | text-align:left; 133 | } 134 | 135 | .radio label { 136 | color: #fff; 137 | } 138 | 139 | .sidebar { 140 | position: fixed; 141 | top: 200px; 142 | width: 225px; 143 | } 144 | 145 | .sidebar li { 146 | display: block; 147 | text-align: left; 148 | } 149 | 150 | .sidebar li ul { 151 | padding: 0; 152 | margin: 0; 153 | } 154 | 155 | .sidebar li ul li { 156 | border-left: 1px solid #fff; 157 | margin: 5px 0; 158 | padding-left: 15px; 159 | } 160 | 161 | .sidebar li ul li a { 162 | font-size: 14px; 163 | } 164 | 165 | .sidebar .nav { 166 | margin:0; 167 | padding:0; 168 | } 169 | 170 | #pooling-example img { 171 | width: 100%; 172 | height: auto; 173 | } 174 | 175 | #pooling-example .row { 176 | margin-bottom: 30px; 177 | } 178 | -------------------------------------------------------------------------------- /examples/pooling.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | watermark.js - canvas pooling 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 |

watermark.js

20 | 33 | 34 |

35 | watermark.js leverages canvas pooling so canvases are reused when possible 36 |

37 | 38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
 56 |           
 57 | /**
 58 |  * Place a watermark
 59 |  */
 60 | function placeMark(i) {
 61 |   watermark(['/img/shepherd.jpg', '/img/logo.png'])
 62 |     .image(watermark.image.lowerRight())
 63 |     .then(function (img) {
 64 |       var container = document.getElementById('image-' + (i + 1));
 65 |       container.appendChild(img);
 66 |     });
 67 | }
 68 | 
 69 | /**
 70 |  * Queue up a bunch of async watermark operations
 71 |  */
 72 |  for(var i = 0; i < 9; i++) {
 73 |    setTimeout((function(index) {
 74 |      return function() {
 75 |        placeMark(index);
 76 |      };
 77 |    })(i), 10);
 78 |  }
 79 |           
 80 |         
81 |
82 |
83 |
84 | 85 | 106 | 107 | 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | watermark.js - watermarks in the browser 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 |

watermark.js

20 | 33 | 34 |
35 | 36 |
37 |

Composite Images

38 |
 39 |           
 40 | watermark(['/img/shepherd.jpg', '/img/logo.png'])
 41 |   .image(watermark.image.lowerRight())
 42 |   .then(function (img) {
 43 |     document.getElementById('composite-image').appendChild(img);
 44 |   });
 45 |           
 46 |         
47 |
48 | 49 |
50 |

Alpha Transparency

51 |
 52 |           
 53 | watermark(['/img/forest.jpg', '/img/logo.png'])
 54 |   .image(watermark.image.lowerRight(0.5))
 55 |   .then(function (img) {
 56 |     document.getElementById('alpha-image').appendChild(img);
 57 |   });
 58 |           
 59 |         
60 |
61 | 62 |
63 |

Text

64 |
 65 |           
 66 | watermark(['/img/field.jpg'])
 67 |   .image(watermark.text.lowerRight('MyPhoto', '28px serif', '#fff', 0.5))
 68 |   .then(function (img) {
 69 |     document.getElementById('text').appendChild(img);
 70 |   });
 71 |           
 72 |         
73 |
74 | 75 |
76 |
77 | 78 | 103 | 104 | 105 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /lib/image/index.js: -------------------------------------------------------------------------------- 1 | import {identity} from '../functions' 2 | 3 | /** 4 | * Set the src of an image object and call the resolve function 5 | * once it has loaded 6 | * 7 | * @param {Image} img 8 | * @param {String} src 9 | * @param {Function} resolve 10 | */ 11 | function setAndResolve(img, src, resolve) { 12 | img.onload = () => resolve(img); 13 | img.src = src; 14 | } 15 | 16 | /** 17 | * Given a resource, return an appropriate loading function for it's type 18 | * 19 | * @param {String|File|Image} resource 20 | * @return {Function} 21 | */ 22 | export function getLoader(resource) { 23 | const type = typeof(resource); 24 | 25 | if (type === 'string') { 26 | return loadUrl; 27 | } 28 | 29 | if (resource instanceof Image) { 30 | return identity; 31 | } 32 | 33 | return loadFile; 34 | } 35 | 36 | /** 37 | * Used for loading image resources asynchronously and maintaining 38 | * the supplied order of arguments 39 | * 40 | * @param {Array} resources - a mixed array of urls, File objects, or Image objects 41 | * @param {Function} init - called at the beginning of resource initialization 42 | * @return {Promise} 43 | */ 44 | export function load(resources, init) { 45 | let promises = []; 46 | for (var i = 0; i < resources.length; i++) { 47 | const resource = resources[i]; 48 | const loader = getLoader(resource); 49 | const promise = loader(resource, init); 50 | promises.push(promise); 51 | } 52 | return Promise.all(promises); 53 | } 54 | 55 | /** 56 | * Load an image by its url 57 | * 58 | * @param {String} url 59 | * @param {Function} init - an optional image initializer 60 | * @return {Promise} 61 | */ 62 | export function loadUrl(url, init) { 63 | const img = new Image(); 64 | (typeof(init) === 'function') && init(img); 65 | return new Promise(resolve => { 66 | img.onload = () => resolve(img) 67 | img.src = url; 68 | }); 69 | } 70 | 71 | /** 72 | * Return a collection of images from an 73 | * array of File objects 74 | * 75 | * @param {File} file 76 | * @return {Promise} 77 | */ 78 | export function loadFile(file) { 79 | const reader = new FileReader(); 80 | return new Promise(resolve => { 81 | const img = new Image(); 82 | reader.onloadend = () => setAndResolve(img, reader.result, resolve); 83 | reader.readAsDataURL(file); 84 | }); 85 | } 86 | 87 | /** 88 | * Create a new image, optionally configuring it's onload behavior 89 | * 90 | * @param {String} url 91 | * @param {Function} onload 92 | * @return {Image} 93 | */ 94 | export function createImage(url, onload) { 95 | const img = new Image(); 96 | if (typeof(onload) === 'function') { 97 | img.onload = onload; 98 | } 99 | img.src = url; 100 | return img; 101 | } 102 | 103 | /** 104 | * Draw an image to a canvas element 105 | * 106 | * @param {Image} img 107 | * @param {HTMLCanvasElement} canvas 108 | * @return {HTMLCanvasElement} 109 | */ 110 | function drawImage(img, canvas) { 111 | const ctx = canvas.getContext('2d'); 112 | 113 | canvas.width = img.width; 114 | canvas.height = img.height; 115 | ctx.drawImage(img, 0, 0); 116 | return canvas; 117 | } 118 | 119 | /** 120 | * Convert an Image object to a canvas 121 | * 122 | * @param {Image} img 123 | * @param {CanvasPool} pool 124 | * @return {HTMLCanvasElement} 125 | */ 126 | export function imageToCanvas(img, pool) { 127 | const canvas = pool.pop(); 128 | return drawImage(img, canvas); 129 | } 130 | 131 | /** 132 | * Convert an array of image objects 133 | * to canvas elements 134 | * 135 | * @param {Array} images 136 | * @param {CanvasPool} pool 137 | * @return {HTMLCanvasElement[]} 138 | */ 139 | export function mapToCanvas(images, pool) { 140 | return images.map(img => imageToCanvas(img, pool)); 141 | } 142 | -------------------------------------------------------------------------------- /lib/style/text/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Return a function for positioning a watermark on a target canvas 3 | * 4 | * @param {Function} xFn - a function to determine an x value 5 | * @param {Function} yFn - a function to determine a y value 6 | * @param {String} text - the text to write 7 | * @param {String} font - same as the CSS font property 8 | * @param {String} fillStyle 9 | * @param {Number} alpha 10 | * @return {Function} 11 | */ 12 | export function atPos(xFn, yFn, text, font, fillStyle, alpha) { 13 | alpha || (alpha = 1.0); 14 | return function (target) { 15 | const context = target.getContext('2d'); 16 | context.save(); 17 | 18 | context.globalAlpha = alpha; 19 | context.fillStyle = fillStyle; 20 | context.font = font; 21 | let metrics = context.measureText(text); 22 | context.fillText(text, xFn(target, metrics, context), yFn(target, metrics, context)); 23 | 24 | context.restore(); 25 | return target; 26 | } 27 | } 28 | 29 | /** 30 | * Write text to the lower right corner of the target canvas 31 | * 32 | * @param {String} text - the text to write 33 | * @param {String} font - same as the CSS font property 34 | * @param {String} fillStyle 35 | * @param {Number} alpha - control text transparency 36 | * @param {Number} y - height in text metrics is not very well supported. This is a manual value 37 | * @return {Function} 38 | */ 39 | export function lowerRight(text, font, fillStyle, alpha, y) { 40 | return atPos( 41 | (target, metrics) => target.width - (metrics.width + 10), 42 | target => y || (target.height - 10), 43 | text, 44 | font, 45 | fillStyle, 46 | alpha 47 | ); 48 | } 49 | 50 | /** 51 | * Write text to the lower left corner of the target canvas 52 | * 53 | * @param {String} text - the text to write 54 | * @param {String} font - same as the CSS font property 55 | * @param {String} fillStyle 56 | * @param {Number} alpha - control text transparency 57 | * @param {Number} y - height in text metrics is not very well supported. This is a manual value 58 | * @return {Function} 59 | */ 60 | export function lowerLeft(text, font, fillStyle, alpha, y) { 61 | return atPos( 62 | () => 10, 63 | target => y || (target.height - 10), 64 | text, 65 | font, 66 | fillStyle, 67 | alpha 68 | ); 69 | } 70 | 71 | /** 72 | * Write text to the upper right corner of the target canvas 73 | * 74 | * @param {String} text - the text to write 75 | * @param {String} font - same as the CSS font property 76 | * @param {String} fillStyle 77 | * @param {Number} alpha - control text transparency 78 | * @param {Number} y - height in text metrics is not very well supported. This is a manual value 79 | * @return {Function} 80 | */ 81 | export function upperRight(text, font, fillStyle, alpha, y) { 82 | return atPos( 83 | (target, metrics) => target.width - (metrics.width + 10), 84 | () => y || 20, 85 | text, 86 | font, 87 | fillStyle, 88 | alpha 89 | ); 90 | } 91 | 92 | /** 93 | * Write text to the upper left corner of the target canvas 94 | * 95 | * @param {String} text - the text to write 96 | * @param {String} font - same as the CSS font property 97 | * @param {String} fillStyle 98 | * @param {Number} alpha - control text transparency 99 | * @param {Number} y - height in text metrics is not very well supported. This is a manual value 100 | * @return {Function} 101 | */ 102 | export function upperLeft(text, font, fillStyle, alpha, y) { 103 | return atPos( 104 | () => 10, 105 | () => y || 20, 106 | text, 107 | font, 108 | fillStyle, 109 | alpha 110 | ); 111 | } 112 | 113 | /** 114 | * Write text to the center of the target canvas 115 | * 116 | * @param {String} text - the text to write 117 | * @param {String} font - same as the CSS font property 118 | * @param {String} fillStyle 119 | * @param {Number} alpha - control text transparency 120 | * @param {Number} y - height in text metrics is not very well supported. This is a manual value 121 | * @return {Function} 122 | */ 123 | export function center(text, font, fillStyle, alpha, y) { 124 | return atPos( 125 | (target, metrics, ctx) => {ctx.textAlign = 'center'; return target.width / 2;}, 126 | (target, metrics, ctx) => {ctx.textBaseline = 'middle'; return target.height / 2; }, 127 | text, 128 | font, 129 | fillStyle, 130 | alpha 131 | ); 132 | } 133 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # watermark.js [![Build Status](https://travis-ci.org/brianium/watermarkjs.svg?branch=master)](https://travis-ci.org/brianium/watermarkjs) 2 | 3 | A functional library for watermarking images in the browser. Written with ES6, and made available 4 | to current browsers via [Babel](https://babeljs.io/). Supports urls, file inputs, blobs, and on-page images. 5 | 6 | **Note:** 7 | For anyone that is interested: I ported this to a ClojureScript library called 8 | [Dandy Roll](https://github.com/brianium/dandy-roll). 9 | 10 | ## Tested Browsers 11 | 12 | Any browser supporting [File](https://developer.mozilla.org/en-US/docs/Web/API/File#Browser_compatibility) and [FileReader](https://developer.mozilla.org/en-US/docs/Web/API/FileReader#Browser_compatibility) should work. The following browsers have been 13 | tested and work: 14 | 15 | * IE10 (Windows 7) 16 | * Chrome 42 (OS X 10.10.3) 17 | * Firefox 38 (OS X 10.10.3) 18 | * Safari 8.0.6 (OS X 10.10.3) 19 | * Opera 29.0 (OS X 10.10.3) 20 | 21 | Please feel free to update this list or submit a fix for a particular browser via a pull request. 22 | 23 | ## Installing 24 | 25 | watermark.js is available via npm and bower: 26 | 27 | ``` 28 | # install via npm 29 | $ npm install watermarkjs 30 | 31 | # install via bower 32 | $ bower install watermarkjs 33 | ``` 34 | 35 | ## Usage 36 | 37 | ```js 38 | // watermark by local path 39 | watermark(['img/photo.jpg', 'img/logo.png']) 40 | .image(watermark.image.lowerRight(0.5)) 41 | .then(img => document.getElementById('container').appendChild(img)); 42 | 43 | // load a url and file object 44 | const upload = document.querySelector('input[type=file]').files[0]; 45 | watermark([upload, 'img/logo.png']) 46 | .image(watermark.image.lowerLeft(0.5)) 47 | .then(img => document.getElementById('container').appendChild(img)); 48 | 49 | // watermark from remote source 50 | const options = { 51 | init(img) { 52 | img.crossOrigin = 'anonymous' 53 | } 54 | }; 55 | watermark(['http://host.com/photo.jpg', 'http://host.com/logo.png'], options) 56 | .image(watermark.image.lowerRight(0.5)) 57 | .then(img => document.getElementById('container').appendChild(img)); 58 | ``` 59 | 60 | ## Building 61 | 62 | Before building or testing, install all the deps: 63 | 64 | ``` 65 | npm i 66 | ``` 67 | 68 | There is an npm script you can run to build: 69 | 70 | ``` 71 | npm run build 72 | ``` 73 | 74 | Or to kick off the file watcher and build as you make changes, run the start task: 75 | 76 | ``` 77 | $ npm start 78 | ``` 79 | 80 | ## Testing 81 | 82 | There is an npm script for that too!: 83 | 84 | ``` 85 | $ npm test 86 | ``` 87 | 88 | This library uses the [Jest](https://facebook.github.io/jest/) testing framework. Due to some current 89 | issues with Jest, Node 0.10.x is required to run the tests. 90 | 91 | ## Examples 92 | 93 | You can view examples and documentation by running the `sync` task via npm: 94 | 95 | ``` 96 | $ npm run sync 97 | ``` 98 | The examples demonstrate using watermark images and text, as well as a demonstration 99 | of uploading a watermarked image to Amazon S3. It is the same content hosted at 100 | [http://brianium.github.io/watermarkjs/](http://brianium.github.io/watermarkjs/). 101 | 102 | ## Development 103 | 104 | Running `npm run dev` will start a browser and start watching source files for changes. 105 | 106 | ## Motivation 107 | 108 | * Not every server has image libraries (shared hosting anyone?) 109 | * Not every server has reliable concurrency libs for efficient uploading (shared hosting anyone?) 110 | * JavaScript is fun and cool - more so with ES6 111 | 112 | Clearly watermarking on the client has some limitations when watermarking urls and on-page elements. The curious can find urls for non-watermarked images, but it is likely that most average users won't go down this path - keeping this soft barrier useful. However!... 113 | 114 | watermark.js has the ability to accept file inputs as a source for watermarking. This makes it easy to preview, watermark, and upload without the non-watermarked image ever becoming public. Check out the [uploading](http://brianium.github.io/watermarkjs/uploading.html) demo to see this in action. 115 | 116 | This tool certainly shines in admin or CMS environments where you want to generate watermarks and upload them asynchronously where it would not be possible or preferable on the server. One less thing the server has to do can be a good thing :) 117 | 118 | ## Suggestions? Improvements? 119 | 120 | Please open issues or pull requests if you have bugs/improvements. 121 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | import {load, mapToCanvas, createImage} from './image'; 2 | import {dataUrl as mapToDataUrl} from './canvas'; 3 | import {blob as mapToBlob} from './blob'; 4 | import * as style from './style'; 5 | import {clone, extend} from './object'; 6 | import pool from './canvas/pool'; 7 | 8 | /** 9 | * A configuration type for the watermark function 10 | * 11 | * @typedef {Object} Options 12 | * @property {Function} init - an initialization function that is given Image objects before loading (only applies if resources is a collection of urls) 13 | * @property {ImageFormat} type - specify the image format to be used when retrieving result (only supports "image/png" or "image/jpeg", default "image/png") 14 | * @property {Number} encoderOptions - specify the image compression quality from 0 to 1 (default 0.92) 15 | * @property {Number} poolSize - number of canvas elements available for drawing, 16 | * @property {CanvasPool} pool - the pool used. If provided, poolSize will be ignored 17 | */ 18 | 19 | /** 20 | * @constant 21 | * @type {Options} 22 | */ 23 | const defaults = { 24 | init: () => {}, 25 | type: 'image/png', 26 | encoderOptions: 0.92 27 | } 28 | 29 | /** 30 | * Merge the given options with the defaults 31 | * 32 | * @param {Options} options 33 | * @return {Options} 34 | */ 35 | function mergeOptions(options) { 36 | return extend(clone(defaults), options); 37 | } 38 | 39 | /** 40 | * Release canvases from a draw result for reuse. Returns 41 | * the dataURL from the result's canvas 42 | * 43 | * @param {DrawResult} result 44 | * @param {CanvasPool} pool 45 | * @return {String} 46 | */ 47 | function release(result, pool, parameters) { 48 | const { canvas, sources } = result; 49 | const dataURL = mapToDataUrl(canvas, parameters); 50 | sources.forEach(pool.release); 51 | return dataURL; 52 | } 53 | 54 | /** 55 | * Return a watermark object 56 | * 57 | * 58 | * @param {Array} resources - a collection of urls, File objects, or Image objects 59 | * @param {Options} options - a configuration object for watermark 60 | * @param {Promise} promise - optional 61 | * @return {Object} 62 | */ 63 | export default function watermark(resources, options = {}, promise = null) { 64 | const opts = mergeOptions(options); 65 | promise || (promise = load(resources, opts.init)); 66 | 67 | return { 68 | /** 69 | * Convert the watermarked image into a dataUrl. The draw 70 | * function is given all images as canvas elements in order 71 | * 72 | * @param {Function} draw 73 | * @return {Object} 74 | */ 75 | dataUrl(draw) { 76 | const promise = this 77 | .then(images => mapToCanvas(images, pool)) 78 | .then(canvases => style.result(draw, canvases)) 79 | .then(result => release(result, pool, { type: opts.type, encoderOptions:opts.encoderOptions })); 80 | 81 | return watermark(resources, opts, promise); 82 | }, 83 | 84 | /** 85 | * Load additional resources 86 | * 87 | * @param {Array} resources - a collection of urls, File objects, or Image objects 88 | * @param {Function} init - an initialization function that is given Image objects before loading (only applies if resources is a collection of urls) 89 | * @return {Object} 90 | */ 91 | load(resources, init) { 92 | const promise = this 93 | .then(resource => load([resource].concat(resources), init)); 94 | 95 | return watermark(resources, opts, promise); 96 | }, 97 | 98 | /** 99 | * Render the current state of the watermarked image. Useful for performing 100 | * actions after the watermark has been applied 101 | * 102 | * @return {Object} 103 | */ 104 | render() { 105 | const promise = this 106 | .then(resource => load([resource])); 107 | 108 | return watermark(resources, opts, promise); 109 | }, 110 | 111 | /** 112 | * Convert the watermark into a blob 113 | * 114 | * @param {Function} draw 115 | * @return {Object} 116 | */ 117 | blob(draw) { 118 | const promise = this.dataUrl(draw) 119 | .then(mapToBlob); 120 | 121 | return watermark(resources, opts, promise); 122 | }, 123 | 124 | /** 125 | * Convert the watermark into an image using the given draw function 126 | * 127 | * @param {Function} draw 128 | * @return {Object} 129 | */ 130 | image(draw) { 131 | const promise = this.dataUrl(draw) 132 | .then(createImage); 133 | 134 | return watermark(resources, opts, promise); 135 | }, 136 | 137 | /** 138 | * Delegate to the watermark promise 139 | * 140 | * @return {Promise} 141 | */ 142 | then(...funcs) { 143 | return promise.then.apply(promise, funcs); 144 | } 145 | }; 146 | }; 147 | 148 | /** 149 | * Style functions 150 | */ 151 | watermark.image = style.image; 152 | watermark.text = style.text; 153 | 154 | /** 155 | * Clean up all canvas references 156 | */ 157 | watermark.destroy = () => pool.clear(); 158 | -------------------------------------------------------------------------------- /dist/watermark.min.js: -------------------------------------------------------------------------------- 1 | !function(n,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.watermark=t():n.watermark=t()}(window,function(){return r={},u.m=e=[function(n,t,e){n.exports=e(1).default},function(n,t,e){"use strict";e.r(t);var r={};e.r(r),e.d(r,"atPos",function(){return s}),e.d(r,"lowerRight",function(){return g}),e.d(r,"upperRight",function(){return y}),e.d(r,"lowerLeft",function(){return v}),e.d(r,"upperLeft",function(){return m}),e.d(r,"center",function(){return w});var u={};function c(n){return n}function f(n){return(f="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(n){return typeof n}:function(n){return n&&"function"==typeof Symbol&&n.constructor===Symbol&&n!==Symbol.prototype?"symbol":typeof n})(n)}function o(n,t){for(var e,r=[],u=0;u 2 | 3 | 4 | 5 | 6 | watermark.js - watermarks in the browser 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 |

watermark.js

20 | 33 | 34 |
35 |
36 |

37 | It would be pretty handy to upload a watermarked image once generated. Doing so in the client is a 38 | great idea, especially if you are uncertain about image library support in a hosting environment. JavaScript is 39 | decent at doing things concurrently as well, so you can even make your application snappier by letting the client 40 | upload multiple images concurrently instead of a hosting environment that may or may not support such things. 41 |

42 | 43 |

44 | The form below allows you to generate a watermarked image and upload it to Amazon S3. The form expects 45 | a an access key id, a policy, and a signature. If you want an easy tool for generating the policy and signature, you can check out policy-signer. 46 | More info on policy documents can be found here. 47 |

48 |
49 |
50 | 51 |
52 |
53 |
54 |
55 | 56 | 57 | Browse… 58 | 59 | 60 | 61 |
62 | 63 | Select the image to watermark 64 | 65 |
66 | 67 | 68 | Browse… 69 | 70 | 71 | 72 |
73 | 74 | Select the watermark image 75 | 76 |
77 |

Position

78 |
79 |
80 |
81 | 82 |
83 |
84 | 85 |
86 |
87 | 88 |
89 |
90 |
91 |
92 | 93 |
94 |
95 | 96 |
97 |
98 |
99 |
100 | 101 |

Amazon S3 Info

102 |
103 | 104 |
105 |
106 | 107 |
108 |
109 | 110 |
111 |
112 | 113 |
114 | 115 | 116 | 117 | 121 | 124 | 127 | 128 |
129 |
130 |
131 |

Preview

132 |
133 |
134 | 135 | 136 | 137 | -------------------------------------------------------------------------------- /examples/scripts/upload.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | 'use strict'; 4 | 5 | /** 6 | * Quick and dirty upload script to demonstrate uploading 7 | * a watermarked image. 8 | */ 9 | 10 | /** 11 | * A variable for storing the cached target image 12 | */ 13 | var original; 14 | 15 | /** 16 | * ids for aws related inputs 17 | */ 18 | var awsFields = ['accessKeyId', 'policy', 'signature', 'bucket']; 19 | 20 | /** 21 | * Enable fields identified by ids 22 | */ 23 | function enableFields(ids) { 24 | ids.forEach(function(id) { 25 | document.getElementById(id).removeAttribute('disabled'); 26 | }) 27 | } 28 | 29 | /** 30 | * Determine if inputs identified by ids have values 31 | */ 32 | function inputsComplete(ids) { 33 | return ids.every(function(id) { 34 | var val = document.getElementById(id); 35 | return !!val.value; 36 | }); 37 | } 38 | 39 | /** 40 | * Given a file input, set the value of the readonly text input associated with it 41 | */ 42 | function setText(input) { 43 | var group = input.parentNode.parentNode.parentNode; 44 | group.querySelector('.form-control').value = input.files[0].name; 45 | } 46 | 47 | /** 48 | * A listener that fires when the target image is selected 49 | */ 50 | function setTarget(file) { 51 | enableFields(['watermark-button']); 52 | Array.prototype.forEach.call(document.querySelectorAll('input[type=radio]'), function (radio) { 53 | radio.removeAttribute('disabled'); 54 | }); 55 | watermark([file]) 56 | .image(function(target) { return target; }) 57 | .then(function (img) { 58 | resetPreviewImage(); 59 | document.getElementById('preview').appendChild(img); 60 | }); 61 | } 62 | 63 | /** 64 | * A listener that fires when the watermark image has been selected 65 | */ 66 | function setWatermark(file) { 67 | var preview = document.getElementById('preview'), 68 | img = document.getElementById('target').files[0], 69 | position = document.querySelector('input[type=radio]:checked').value; 70 | 71 | if (! original) { 72 | original = img; 73 | } 74 | 75 | watermark([original, file]) 76 | .image(watermark.image[position](0.5)) 77 | .then(function(marked) { 78 | 79 | 80 | 81 | resetPreviewImage(); 82 | document.getElementById('preview').appendChild(marked); 83 | 84 | enableFields(awsFields); 85 | }); 86 | } 87 | 88 | 89 | function resetPreviewImage() 90 | { 91 | var imageTag = document.querySelector("#preview img"); 92 | if(imageTag != null) 93 | { 94 | imageTag.remove(); 95 | original = null; 96 | setWatermark(document.getElementById("watermark").files[0]); 97 | 98 | } 99 | } 100 | 101 | /** 102 | * Check if the watermark has been selected 103 | */ 104 | function isWatermarkSelected() { 105 | var watermark = document.getElementById('watermark-name'); 106 | return !!watermark.value; 107 | } 108 | 109 | /** 110 | * Get a FormData object ready for uploading to S3 111 | */ 112 | function getFormData(blob, filename, accessKeyId, policy, signature) { 113 | var fd = new FormData(), 114 | params = { 115 | key: filename, 116 | AWSAccessKeyId: accessKeyId, 117 | acl: 'private', 118 | policy: policy, 119 | signature: signature, 120 | 'Content-Type': '$Content-Type', 121 | file: [blob, 'watermark.png'] 122 | }; 123 | 124 | for (var k in params) { 125 | var args = Array.isArray(params[k]) ? params[k] : [params[k]]; 126 | fd.append.apply(fd, [k].concat(args)); 127 | } 128 | 129 | return fd; 130 | } 131 | 132 | /** 133 | * Perform the upload. 134 | * 135 | * @param {FormData} 136 | * @param {Function} progress handler 137 | * @param {Function} completion handler 138 | * @param {Function} error handler 139 | */ 140 | function upload(onProgress, onComplete, onError) { 141 | var req = new XMLHttpRequest(), 142 | key = "watermark-" + Date.now().toString() + '.png', 143 | img = document.querySelector('#preview img'), 144 | keyId = document.getElementById('accessKeyId'), 145 | policy = document.getElementById('policy'), 146 | signature = document.getElementById('signature'), 147 | bucket = document.getElementById('bucket'); 148 | 149 | watermark([img]) 150 | .blob(function(target) { return target; }) 151 | .then(function(blob) { 152 | var fd = getFormData(blob, key, keyId.value, policy.value, signature.value); 153 | req.open('POST', 'https://' + bucket.value + '.s3.amazonaws.com/', true); 154 | req.upload.onprogress = onProgress; 155 | req.onreadystatechange = function() { 156 | if (req.readyState === 4) { 157 | if (! /Error/.test(req.responseText)) { //simple test for AWS error response 158 | onComplete(); 159 | } else { 160 | onError(req.responseText); 161 | } 162 | } 163 | } 164 | req.addEventListener('error', onError, false); 165 | req.send(fd); 166 | }); 167 | } 168 | 169 | /** 170 | * Run the sample app once dom content has loaded 171 | */ 172 | document.addEventListener('DOMContentLoaded', function () { 173 | 174 | /** 175 | * Handle file selections and position choice 176 | */ 177 | document.addEventListener('change', function (e) { 178 | var input = e.target; 179 | 180 | if (input.type === 'file') { 181 | setText(input); 182 | input.id === 'target' ? setTarget(input.files[0]) : setWatermark(input.files[0]); 183 | } 184 | 185 | if (input.type === 'radio' && isWatermarkSelected()) { 186 | setWatermark(document.getElementById('watermark').files[0]); 187 | } 188 | }); 189 | 190 | /** 191 | * On keyup for aws inputs, check if the others have been filled in. Once all 192 | * have been filled in, enable the upload button 193 | */ 194 | awsFields.forEach(function (id) { 195 | document.getElementById(id).addEventListener('keyup', function () { 196 | if (inputsComplete(awsFields)) { 197 | enableFields(['upload']); 198 | } 199 | }); 200 | }); 201 | 202 | /** 203 | * Handle form submission - i.e actually do the upload 204 | */ 205 | var form = document.getElementById('uploadForm'); 206 | form.addEventListener('submit', function (e) { 207 | var progress = document.getElementById('progress'), 208 | bar = progress.querySelector('.progress-bar'), 209 | complete = document.getElementById('complete'), 210 | err = document.getElementById('error'); 211 | 212 | progress.style.visibility = 'visible'; 213 | 214 | upload(function(e) { 215 | if (e.lengthComputable) { 216 | var percent = (e.loaded / e.total) * 100; 217 | bar.style.width = percent + "%"; 218 | } 219 | }, function () { 220 | complete.style.display = 'block'; 221 | err.style.display = 'none'; 222 | }, function () { 223 | err.style.display = 'block'; 224 | complete.style.display = 'none'; 225 | }); 226 | 227 | e.preventDefault(); 228 | }); 229 | 230 | 231 | }); 232 | 233 | 234 | })(); 235 | -------------------------------------------------------------------------------- /examples/images.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | watermark.js - using images 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 |

watermark.js

20 | 33 | 34 |
35 |

36 | A common use case for watermarking is to lay one image on top of another. The following examples 37 | demonstrate some of the pre-packaged image positioning functions that come with watermark.js. 38 |

39 | 40 |
41 |

Lower Right

42 |
 43 |           
 44 | watermark(['/img/shepherd.jpg', '/img/logo.png'])
 45 |   .image(watermark.image.lowerRight(0.5))
 46 |   .then(function (img) {
 47 |     document.getElementById('lower-right').appendChild(img);
 48 |   });
 49 |           
 50 |         
51 |
52 | 53 |
54 |

Lower Left

55 |
 56 |           
 57 | watermark(['/img/forest.jpg', '/img/logo.png'])
 58 |   .image(watermark.image.lowerLeft(0.5))
 59 |   .then(function (img) {
 60 |     document.getElementById('lower-left').appendChild(img);
 61 |   });
 62 |           
 63 |         
64 |
65 | 66 |
67 |

Upper Right

68 |
 69 |           
 70 | watermark(['/img/field.jpg', '/img/logo.png'])
 71 |   .image(watermark.image.upperRight(0.5))
 72 |   .then(function (img) {
 73 |     document.getElementById('upper-right').appendChild(img);
 74 |   });
 75 |           
 76 |         
77 |
78 | 79 |
80 |

Upper Left

81 |
 82 |           
 83 | watermark(['/img/wolf.jpg', '/img/logo.png'])
 84 |   .image(watermark.image.upperLeft(0.5))
 85 |   .then(function (img) {
 86 |     document.getElementById('upper-left').appendChild(img);
 87 |   });
 88 |           
 89 |         
90 |
91 | 92 |
93 |

Center

94 |
 95 |           
 96 | watermark(['/img/coffee.jpg', '/img/logo.png'])
 97 |   .image(watermark.image.center(0.5))
 98 |   .then(function (img) {
 99 |     document.getElementById('center').appendChild(img);
100 |   });
101 |           
102 |         
103 |
104 | 105 |
106 |

Arbitrary Positions

107 |
108 |           
109 | var getX = function(boat, logo) {
110 |   return 73;
111 | };
112 | 
113 | var getY = function(boat, logo) {
114 |   return 63;
115 | };
116 | 
117 | // atPos is the basis for all positioning functions
118 | watermark(['/img/boat.jpg', '/img/logo.png'])
119 |   .image(watermark.image.atPos(getX, getY, 0.5))
120 |   .then(function (img) {
121 |     document.getElementById('arbitrary').appendChild(img);
122 |   });
123 |           
124 |         
125 |
126 | 127 |
128 |

Multiple Watermarks

129 |
130 |           
131 | // using load to draw additional images
132 | watermark(['/img/boat.jpg', '/img/logo.png'])
133 |   .image(watermark.image.lowerRight(0.5))
134 |   .load(['/img/peridot.png'])
135 |   .image(watermark.image.upperLeft(0.5))
136 |   .then(function (img) {
137 |     document.getElementById('multiple').appendChild(img);
138 |   });
139 |           
140 |         
141 |
142 | 143 |
144 | 145 |
146 | 147 | 214 | 215 | 216 | 217 | 218 | 219 | -------------------------------------------------------------------------------- /examples/text.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | watermark.js - using images 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 |

watermark.js

20 | 33 | 34 |
35 |

36 | Sometimes you just want to write some text on top of an image. The following examples demonstrate 37 | using some of the text functions included with watermark.js. In the interest of saving space, the following 38 | examples assume watermark.text has been aliased to text. A one second timeout is used 39 | on page load to ensure font faces are loaded. 40 |

41 | 42 |
43 |

Lower Right

44 |
 45 |           
 46 | watermark(['/img/shepherd.jpg'])
 47 |   .image(text.lowerRight('watermark.js', '48px Josefin Slab', '#fff', 0.5))
 48 |   .then(function (img) {
 49 |     document.getElementById('lower-right').appendChild(img);
 50 |   });
 51 |           
 52 |         
53 |
54 | 55 |
56 |

Lower Left

57 |
 58 |           
 59 | watermark(['/img/forest.jpg'])
 60 |   .image(text.lowerLeft('watermark.js', '48px Josefin Slab', '#fff', 0.5))
 61 |   .then(function (img) {
 62 |     document.getElementById('lower-left').appendChild(img);
 63 |   });
 64 |           
 65 |         
66 |
67 | 68 |
69 |

Upper Right

70 |
 71 |           
 72 | // TextMetrics objects do not support font height very well
 73 | // so we manually provide a y value of 48 here
 74 | var ur = text.upperRight;
 75 | watermark(['/img/field.jpg'])
 76 |   .image(ur('watermark.js', '48px Josefin Slab', '#fff', 0.5, 48))
 77 |   .then(function (img) {
 78 |     document.getElementById('upper-right').appendChild(img);
 79 |   });
 80 |           
 81 |         
82 |
83 | 84 |
85 |

Upper Left

86 |
 87 |           
 88 | // TextMetrics objects do not support font height very well
 89 | // so we manually provide a y value of 48 here
 90 | var ul = text.upperLeft;
 91 | watermark(['/img/wolf.jpg'])
 92 |   .image(ul('watermark.js', '48px Josefin Slab', '#fff', 0.5, 48))
 93 |   .then(function (img) {
 94 |     document.getElementById('upper-left').appendChild(img);
 95 |   });
 96 |           
 97 |         
98 |
99 | 100 |
101 |

Center

102 |
103 |           
104 | watermark(['/img/coffee.jpg'])
105 |   .image(text.center('watermark.js', '48px Josefin Slab', '#fff', 0.5))
106 |   .then(function (img) {
107 |     document.getElementById('center').appendChild(img);
108 |   });
109 |           
110 |         
111 |
112 | 113 |
114 |

Arbitrary Positions

115 |
116 |           
117 | var x = function(boat, metrics, context) {
118 |   return 73;
119 | };
120 | 
121 | var y = function(boat, metrics, context) {
122 |   return 63;
123 | };
124 | 
125 | pos = text.atPos;
126 | 
127 | watermark(['/img/boat.jpg'])
128 |   .image(pos(x, y, 'watermark.js', '48px Josefin Slab', '#fff', 0.5))
129 |   .then(function (img) {
130 |     document.getElementById('arbitrary').appendChild(img);
131 |   });
132 |           
133 |         
134 |
135 | 136 |
137 |

Rotate

138 |
139 |           
140 | // At the end of the day, a draw function is just a function
141 | // that receives a canvas, manipulates it, then returns it.
142 | // You can always roll your own functionality.
143 | var rotate = function(target) {
144 |   var context = target.getContext('2d');
145 |   var text = 'watermark.js';
146 |   var metrics = context.measureText(text);
147 |   var x = (target.width / 2) - (metrics.width + 24);
148 |   var y = (target.height / 2) + 48 * 2;
149 | 
150 |   context.translate(x, y);
151 |   context.globalAlpha = 0.5;
152 |   context.fillStyle = '#fff';
153 |   context.font = '48px Josefin Slab';
154 |   context.rotate(-45 * Math.PI / 180);
155 |   context.fillText(text, 0, 0);
156 |   return target;
157 | };
158 | 
159 | watermark(['/img/bear.jpg'])
160 |   .image(rotate)
161 |   .then(function (img) {
162 |     document.getElementById('rotate').appendChild(img);
163 |   });
164 |           
165 |         
166 |
167 | 168 |
169 |

Multiple Text Watermarks

170 |
171 |           
172 | // by chaining off of render() we can write on top
173 | // of our image that has already been watermarked
174 | var ul = text.upperLeft;
175 | watermark(['/img/shepherd.jpg'])
176 |   .image(text.lowerRight('watermark.js', '48px Josefin Slab', '#fff', 0.5))
177 |   .render()
178 |   .image(ul('watermark.js', '48px Josefin Slab', '#fff', 0.5, 48))
179 |   .then(function (img) {
180 |     document.getElementById('multiple').appendChild(img);
181 |   });
182 |           
183 |         
184 |
185 | 186 |
187 | 188 |
189 | 190 | 280 | 281 | 282 | 283 | 284 | 285 | -------------------------------------------------------------------------------- /examples/docs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | watermark.js - api documentation 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 |

watermark.js

18 | 31 | 32 |
33 |
34 | 35 |

watermark(resources, [, options, [, promise]])

36 |

37 | The watermark factory. Loads resources and returns an object with functions 38 | for manipulating them. resources can be a mixed array of any of the 39 | following: url, File, Blob, or Image. The options 40 | object is used to configure the watermark factory. It supports an init 41 | function that is used when loading images from a url, and is invoked with the image before fetching. 42 | It also allows type and encoderOptions to modify the resulting image according 43 | to HTMLCanvasElement.toDataURL() paramters. 44 |

45 |
 46 |           
 47 | // load local images
 48 | watermark(['/img/coffee.jpg', '/img/logo.png']);
 49 | 
 50 | // load cross domain images
 51 | var options = {
 52 |   init: function (img) {
 53 |     img.crossOrigin = 'anonymous';
 54 |   }
 55 | };
 56 | watermark(['http://web.com/a.jpg', 'http://web.com/b.jpg'], options);
 57 | 
 58 | // export resulting image as jpeg and lower the quality
 59 | var options = {
 60 |   type: 'image/jpeg',
 61 |   encoderOptions: 0.6
 62 | };
 63 | watermark(['http://web.com/a.jpg'], options);
 64 | 
 65 | // load a url and a file object
 66 | var upload = document.querySelector('input[type=file]').files[0];
 67 | watermark(['/img/photo.jpg', upload]);
 68 |           
 69 |         
70 | 71 |

image(draw)

72 |

73 | Converts all resources into an Image object by applying the function draw. The draw 74 | function receives all resources as canvases. Draw functions are expected to return the resource being watermarked. 75 |

76 |
 77 |           
 78 | // place a watermark in the upper left hand corner of an image
 79 | watermark(['/img/coffee.jpg', '/img/logo.png'])
 80 |   .image(function (coffee, logo) {
 81 |     var context = coffee.getContext('2d');
 82 |     context.save();
 83 | 
 84 |     context.globalAlpha = alpha;
 85 |     context.drawImage(logo, 10, 10);
 86 | 
 87 |     context.restore();
 88 |     return target;
 89 |   });
 90 |           
 91 |         
92 | 93 |

blob(draw)

94 |

95 | Converts all resources into a Blob object by applying the function draw. The draw 96 | function receives all resources as canvases. Draw functions are expected to return the resource being watermarked. Blob 97 | objects are useful for uploading generated watermarks. 98 |

99 |
100 |           
101 | // place a watermark in the upper left hand corner of an image
102 | watermark(['/img/coffee.jpg', '/img/logo.png'])
103 |   .blob(function (coffee, logo) {
104 |     var context = coffee.getContext('2d');
105 |     context.save();
106 | 
107 |     context.globalAlpha = alpha;
108 |     context.drawImage(logo, 10, 10);
109 | 
110 |     context.restore();
111 |     return target;
112 |   });
113 |           
114 |         
115 | 116 |

dataUrl(draw)

117 |

118 | Converts all resources into a data url by applying the function draw. The draw 119 | function receives all resources as canvases. This function is used to create the base case for the image() and blob() 120 | functions. 121 |

122 |
123 |           
124 | // place a watermark in the upper left hand corner of an image
125 | watermark(['/img/coffee.jpg', '/img/logo.png'])
126 |   .dataUrl(function (coffee, logo) {
127 |     var context = coffee.getContext('2d');
128 |     context.save();
129 | 
130 |     context.globalAlpha = alpha;
131 |     context.drawImage(logo, 10, 10);
132 | 
133 |     context.restore();
134 |     return target;
135 |   });
136 |           
137 |         
138 | 139 |

then(fn)

140 |

141 | Delegates to the Promise that was passed to the watermark() factory. This function is used 142 | to do something with the result of watermarking. 143 |

144 |
145 |           
146 | // append a generated image
147 | watermark(['/img/coffee.jpg', '/img/logo.png'])
148 |   .image(watermark.image.lowerRight(0.5))
149 |   .then(function (img) {
150 |     document.getElementById('preview').appendChild(img);
151 |   });
152 | 
153 | // upload a watermarked image
154 | watermark(['/img/coffee.jpg', '/img/logo.png'])
155 |   .blob(watermark.text.lowerRight('watermark.js', '48px serif', '#fff', 0.5))
156 |   .then(function (blob) {
157 |     // perform upload using FormData
158 |   });
159 | 
160 | // use a data url as an image source
161 | watermark(['/img/coffee.jpg', '/img/logo.png'])
162 |   .dataUrl(watermark.image.lowerLeft(0.5))
163 |   .then(function (url) {
164 |     document.querySelector('img').src = url;
165 |   });
166 |           
167 |         
168 | 169 |

load(resources, [init])

170 |

171 | Load additional resources. resources and init behave just as they do 172 | in the watermark() factory. This function is useful for loading images on top 173 | of already generated watermarks. 174 |

175 |
176 |           
177 | // one watermark in the lower right and one in the upper left
178 | watermark(['/img/boat.jpg', '/img/logo.png'])
179 |   .image(watermark.image.lowerRight(0.5))
180 |   .load(['/img/other-logo.png'])
181 |   .image(watermark.image.upperLeft(0.5))
182 |   .then(function (img) {
183 |     document.getElementById('#preview').appendChild(img);
184 |   });
185 |           
186 |         
187 | 188 |

render()

189 |

190 | Apply all transformations to the watermarked image and load it as a resource. Useful for general 191 | post-processing or writing additional text on a watermarked image. 192 |

193 |
194 |           
195 | // write multiple text watermarks
196 | var text = watermark.text
197 | watermark(['/img/shepherd.jpg'])
198 |   .image(text.lowerRight('watermark.js', '48px Josefin Slab', '#fff', 0.5))
199 |   .render()
200 |   .image(text.upperLeft('watermark.js', '48px Josefin Slab', '#fff', 0.5, 48))
201 |   .then(function (img) {
202 |     document.getElementById('#preview').appendChild(img);
203 |   });
204 |           
205 |         
206 | 207 |

destroy()

208 | 209 |

210 | watermark.js makes use of a canvas pool to reuse resources as much as possible. This is accomplished 211 | with a shared pool. The destroy() function on the watermark() factory explicitly destroys all remaining 212 | canvas references in the pool and should be used to clean up after doing a bunch of watermark operations. 213 |

214 | 215 |
216 |           
217 | // remove any remaining canvas references
218 | watermark.destroy();
219 |           
220 |         
221 | 222 |

Styles

223 | 224 |

225 | As noted about draw functions, they accept canvas objects and perform various operations on them 226 | to compose a single image. watermark.js comes with two namespaces featuring common draw operations for 227 | watermarks. 228 |

229 | 230 |

watermark.image

231 | 232 |

233 | This namespace contains functions for positioning watermark images and controlling opacity. Most of these functions 234 | accept an alpha value and return a positioning function. 235 |

236 | 237 |
238 |           
239 | // draw the logo at the lower right corner at 50% opacity
240 | watermark(['/img/coffee.jpg', '/img/logo.png'])
241 |   .image(watermark.image.lowerRight(0.5));
242 | 
243 | // draw the logo at the lower left corner at 50% opacity
244 | watermark(['/img/coffee.jpg', '/img/logo.png'])
245 |   .image(watermark.image.lowerLeft(0.5));
246 | 
247 | // draw the logo at the upper right corner at 50% opacity
248 | watermark(['/img/coffee.jpg', '/img/logo.png'])
249 |   .image(watermark.image.upperRight(0.5));
250 | 
251 | // draw the logo at the upper left corner at 50% opacity
252 | watermark(['/img/coffee.jpg', '/img/logo.png'])
253 |   .image(watermark.image.upperLeft(0.5));
254 | 
255 | // draw the logo at the center at 50% opacity
256 | watermark(['/img/coffee.jpg', '/img/logo.png'])
257 |   .image(watermark.image.center(0.5));
258 | 
259 | /**
260 |  * Callback for determining the x coordinate of the watermark
261 |  *
262 |  * @param {HTMLCanvasElement} coffee
263 |  * @param {HTMLCanvasElement} logo
264 |  * @return {Number}
265 |  */
266 | var getX = function(coffee, logo) {
267 |   return 73;
268 | };
269 | 
270 | /**
271 |  * Callback for determining the y coordinate of the watermark
272 |  *
273 |  * @param {HTMLCanvasElement} coffee
274 |  * @param {HTMLCanvasElement} logo
275 |  * @return {Number}
276 |  */
277 | var getY = function(coffee, logo) {
278 |   return 63;
279 | };
280 | 
281 | // draw the logo at an arbitrary location at 60% opacity
282 | watermark(['/img/coffee.jpg', '/img/logo.png'])
283 |   .image(watermark.image.atPos(getX, getY, 0.6));
284 |           
285 |         
286 | 287 | 288 |

watermark.text

289 | 290 |

291 | This namespace contains functions for positioning watermark text and controlling opacity. Most of these functions 292 | accept font styles and an alpha value and return a positioning function. 293 |

294 | 295 |
296 |           
297 | // draw 48px white text at the lower right corner at 50% opacity
298 | watermark(['/img/coffee.jpg'])
299 |   .image(watermark.text.lowerRight('watermark.js', '48px sans-serif', '#fff', 0.5));
300 | 
301 | // draw 48px white text at the lower left corner at 50% opacity
302 | watermark(['/img/coffee.jpg'])
303 |   .image(watermark.text.lowerLeft('watermark.js', '48px sans-serif', '#fff', 0.5));
304 | 
305 | /**
306 |  * Note: functions for drawing to the upper right and left corners
307 |  * support an additional y parameter due to browser limitations of
308 |  * the TextMetrics object used to calculate font height. When in doubt
309 |  * just use the size of your font for this parameter.
310 |  */
311 | 
312 | // draw 48px white text at the upper right corner at 50% opacity
313 | watermark(['/img/coffee.jpg'])
314 |   .image(watermark.text.upperRight('watermark.js', '48px sans-serif', '#fff', 0.5, 48));
315 | 
316 | // draw 48px white text at the upper left corner at 50% opacity
317 | watermark(['/img/coffee.jpg'])
318 |   .image(watermark.text.upperLeft('watermark.js', '48px sans-serif', '#fff', 0.5, 48));
319 | 
320 | // draw 48px white text at the center at 50% opacity
321 | watermark(['/img/coffee.jpg'])
322 |   .image(watermark.text.center('watermark.js', '48px sans-serif', '#fff', 0.5));
323 | 
324 | /**
325 |  * Callback for determining the x coordinate of the watermark
326 |  *
327 |  * @param {HTMLCanvasElement} coffee
328 |  * @param {TextMetrics} metrics
329 |  * @param {CanvasRenderingContex2D} context - context of the coffee canvas
330 |  * @return {Number}
331 |  */
332 | var x = function(coffee, metrics, context) {
333 |   return 73;
334 | };
335 | 
336 | /**
337 |  * Callback for determining the y coordinate of the watermark
338 |  *
339 |  * @param {HTMLCanvasElement} coffee
340 |  * @param {TextMetrics} metrics
341 |  * @param {CanvasRenderingContex2D} context - context of the coffee canvas
342 |  * @return {Number}
343 |  */
344 | var y = function(coffee, metrics, context) {
345 |   return 63;
346 | };
347 | 
348 | // draw 48px white text at an arbitrary location at 60% opacity
349 | watermark(['/img/coffee.jpg'])
350 |   .image(watermark.text.atPos(x, y, 'watermark.js', '48px sans-serif', '#fff', 0.6));
351 |           
352 |         
353 | 354 |

Custom Styles

355 | 356 |

357 | Creating your own custom styles is easy. Since watermark.js subscribes to the functional 358 | style, a good pattern is to write functions that return functions. 359 |

360 | 361 |
362 |   
363 | watermark(['/img/coffee.jpg'])
364 |   image(myRadRotateFunction(0.5));
365 |   
366 | 
367 | 368 |

369 | Keep in mind that these draw functions receive canvas objects, so in order to keep them 370 | composable it is a good idea to leave the context the way you found it. 371 |

372 | 373 |
374 |   
375 | function myRadRotateFunction(alpha) {
376 |   return function(target) {
377 |     var context = target.getContext('2d');
378 |     context.save(); // capture the state of the context
379 | 
380 |     // do some cool rotating things
381 | 
382 |     context.restore(); // put the context back where you found it
383 |     return target;
384 |   }
385 | }
386 |   
387 | 
388 | 389 |

390 | Note that we return the target canvas after we are done. This is required in order 391 | for watermark.js to pass the finished product to then when you are ready 392 | to do something with your watermarked image. 393 |

394 | 395 |
396 |
397 | 417 |
418 |
419 | 420 | 421 | 422 | 423 | 424 | 425 | -------------------------------------------------------------------------------- /examples/css/bootstrap-theme.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.4 (http://getbootstrap.com) 3 | * Copyright 2011-2015 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | 7 | /*! 8 | * Generated using the Bootstrap Customizer (http://getbootstrap.com/customize/?id=c027d64f76aef04286a2) 9 | * Config saved to config.json and https://gist.github.com/c027d64f76aef04286a2 10 | */.btn-default,.btn-primary,.btn-success,.btn-info,.btn-warning,.btn-danger{text-shadow:0 -1px 0 rgba(0,0,0,0.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 1px rgba(0,0,0,0.075)}.btn-default:active,.btn-primary:active,.btn-success:active,.btn-info:active,.btn-warning:active,.btn-danger:active,.btn-default.active,.btn-primary.active,.btn-success.active,.btn-info.active,.btn-warning.active,.btn-danger.active{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn-default .badge,.btn-primary .badge,.btn-success .badge,.btn-info .badge,.btn-warning .badge,.btn-danger .badge{text-shadow:none}.btn:active,.btn.active{background-image:none}.btn-default{background-image:-webkit-linear-gradient(top, #fff 0, #e0e0e0 100%);background-image:-o-linear-gradient(top, #fff 0, #e0e0e0 100%);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0, #fff), to(#e0e0e0));background-image:linear-gradient(to bottom, #fff 0, #e0e0e0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);background-repeat:repeat-x;border-color:#dbdbdb;text-shadow:0 1px 0 #fff;border-color:#ccc}.btn-default:hover,.btn-default:focus{background-color:#e0e0e0;background-position:0 -15px}.btn-default:active,.btn-default.active{background-color:#e0e0e0;border-color:#dbdbdb}.btn-default.disabled,.btn-default:disabled,.btn-default[disabled]{background-color:#e0e0e0;background-image:none}.btn-primary{background-image:-webkit-linear-gradient(top, #337ab7 0, #265a88 100%);background-image:-o-linear-gradient(top, #337ab7 0, #265a88 100%);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0, #337ab7), to(#265a88));background-image:linear-gradient(to bottom, #337ab7 0, #265a88 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);background-repeat:repeat-x;border-color:#245580}.btn-primary:hover,.btn-primary:focus{background-color:#265a88;background-position:0 -15px}.btn-primary:active,.btn-primary.active{background-color:#265a88;border-color:#245580}.btn-primary.disabled,.btn-primary:disabled,.btn-primary[disabled]{background-color:#265a88;background-image:none}.btn-success{background-image:-webkit-linear-gradient(top, #5cb85c 0, #419641 100%);background-image:-o-linear-gradient(top, #5cb85c 0, #419641 100%);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0, #5cb85c), to(#419641));background-image:linear-gradient(to bottom, #5cb85c 0, #419641 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);background-repeat:repeat-x;border-color:#3e8f3e}.btn-success:hover,.btn-success:focus{background-color:#419641;background-position:0 -15px}.btn-success:active,.btn-success.active{background-color:#419641;border-color:#3e8f3e}.btn-success.disabled,.btn-success:disabled,.btn-success[disabled]{background-color:#419641;background-image:none}.btn-info{background-image:-webkit-linear-gradient(top, #5bc0de 0, #2aabd2 100%);background-image:-o-linear-gradient(top, #5bc0de 0, #2aabd2 100%);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0, #5bc0de), to(#2aabd2));background-image:linear-gradient(to bottom, #5bc0de 0, #2aabd2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);background-repeat:repeat-x;border-color:#28a4c9}.btn-info:hover,.btn-info:focus{background-color:#2aabd2;background-position:0 -15px}.btn-info:active,.btn-info.active{background-color:#2aabd2;border-color:#28a4c9}.btn-info.disabled,.btn-info:disabled,.btn-info[disabled]{background-color:#2aabd2;background-image:none}.btn-warning{background-image:-webkit-linear-gradient(top, #f0ad4e 0, #eb9316 100%);background-image:-o-linear-gradient(top, #f0ad4e 0, #eb9316 100%);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0, #f0ad4e), to(#eb9316));background-image:linear-gradient(to bottom, #f0ad4e 0, #eb9316 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);background-repeat:repeat-x;border-color:#e38d13}.btn-warning:hover,.btn-warning:focus{background-color:#eb9316;background-position:0 -15px}.btn-warning:active,.btn-warning.active{background-color:#eb9316;border-color:#e38d13}.btn-warning.disabled,.btn-warning:disabled,.btn-warning[disabled]{background-color:#eb9316;background-image:none}.btn-danger{background-image:-webkit-linear-gradient(top, #d9534f 0, #c12e2a 100%);background-image:-o-linear-gradient(top, #d9534f 0, #c12e2a 100%);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0, #d9534f), to(#c12e2a));background-image:linear-gradient(to bottom, #d9534f 0, #c12e2a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);background-repeat:repeat-x;border-color:#b92c28}.btn-danger:hover,.btn-danger:focus{background-color:#c12e2a;background-position:0 -15px}.btn-danger:active,.btn-danger.active{background-color:#c12e2a;border-color:#b92c28}.btn-danger.disabled,.btn-danger:disabled,.btn-danger[disabled]{background-color:#c12e2a;background-image:none}.thumbnail,.img-thumbnail{-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.075);box-shadow:0 1px 2px rgba(0,0,0,0.075)}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{background-image:-webkit-linear-gradient(top, #f5f5f5 0, #e8e8e8 100%);background-image:-o-linear-gradient(top, #f5f5f5 0, #e8e8e8 100%);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0, #f5f5f5), to(#e8e8e8));background-image:linear-gradient(to bottom, #f5f5f5 0, #e8e8e8 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-color:#e8e8e8}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{background-image:-webkit-linear-gradient(top, #337ab7 0, #2e6da4 100%);background-image:-o-linear-gradient(top, #337ab7 0, #2e6da4 100%);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0, #337ab7), to(#2e6da4));background-image:linear-gradient(to bottom, #337ab7 0, #2e6da4 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-color:#2e6da4}.navbar-default{background-image:-webkit-linear-gradient(top, #fff 0, #f8f8f8 100%);background-image:-o-linear-gradient(top, #fff 0, #f8f8f8 100%);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0, #fff), to(#f8f8f8));background-image:linear-gradient(to bottom, #fff 0, #f8f8f8 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);border-radius:4px;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 5px rgba(0,0,0,0.075);box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 5px rgba(0,0,0,0.075)}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.active>a{background-image:-webkit-linear-gradient(top, #dbdbdb 0, #e2e2e2 100%);background-image:-o-linear-gradient(top, #dbdbdb 0, #e2e2e2 100%);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0, #dbdbdb), to(#e2e2e2));background-image:linear-gradient(to bottom, #dbdbdb 0, #e2e2e2 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0);-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,0.075);box-shadow:inset 0 3px 9px rgba(0,0,0,0.075)}.navbar-brand,.navbar-nav>li>a{text-shadow:0 1px 0 rgba(255,255,255,0.25)}.navbar-inverse{background-image:-webkit-linear-gradient(top, #3c3c3c 0, #222 100%);background-image:-o-linear-gradient(top, #3c3c3c 0, #222 100%);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0, #3c3c3c), to(#222));background-image:linear-gradient(to bottom, #3c3c3c 0, #222 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false)}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.active>a{background-image:-webkit-linear-gradient(top, #080808 0, #0f0f0f 100%);background-image:-o-linear-gradient(top, #080808 0, #0f0f0f 100%);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0, #080808), to(#0f0f0f));background-image:linear-gradient(to bottom, #080808 0, #0f0f0f 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0);-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,0.25);box-shadow:inset 0 3px 9px rgba(0,0,0,0.25)}.navbar-inverse .navbar-brand,.navbar-inverse .navbar-nav>li>a{text-shadow:0 -1px 0 rgba(0,0,0,0.25)}.navbar-static-top,.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}@media (max-width:767px){.navbar .navbar-nav .open .dropdown-menu>.active>a,.navbar .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar .navbar-nav .open .dropdown-menu>.active>a:focus{color:#fff;background-image:-webkit-linear-gradient(top, #337ab7 0, #2e6da4 100%);background-image:-o-linear-gradient(top, #337ab7 0, #2e6da4 100%);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0, #337ab7), to(#2e6da4));background-image:linear-gradient(to bottom, #337ab7 0, #2e6da4 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0)}}.alert{text-shadow:0 1px 0 rgba(255,255,255,0.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.25),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 1px 0 rgba(255,255,255,0.25),0 1px 2px rgba(0,0,0,0.05)}.alert-success{background-image:-webkit-linear-gradient(top, #dff0d8 0, #c8e5bc 100%);background-image:-o-linear-gradient(top, #dff0d8 0, #c8e5bc 100%);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0, #dff0d8), to(#c8e5bc));background-image:linear-gradient(to bottom, #dff0d8 0, #c8e5bc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);border-color:#b2dba1}.alert-info{background-image:-webkit-linear-gradient(top, #d9edf7 0, #b9def0 100%);background-image:-o-linear-gradient(top, #d9edf7 0, #b9def0 100%);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0, #d9edf7), to(#b9def0));background-image:linear-gradient(to bottom, #d9edf7 0, #b9def0 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);border-color:#9acfea}.alert-warning{background-image:-webkit-linear-gradient(top, #fcf8e3 0, #f8efc0 100%);background-image:-o-linear-gradient(top, #fcf8e3 0, #f8efc0 100%);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0, #fcf8e3), to(#f8efc0));background-image:linear-gradient(to bottom, #fcf8e3 0, #f8efc0 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);border-color:#f5e79e}.alert-danger{background-image:-webkit-linear-gradient(top, #f2dede 0, #e7c3c3 100%);background-image:-o-linear-gradient(top, #f2dede 0, #e7c3c3 100%);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0, #f2dede), to(#e7c3c3));background-image:linear-gradient(to bottom, #f2dede 0, #e7c3c3 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);border-color:#dca7a7}.progress{background-image:-webkit-linear-gradient(top, #ebebeb 0, #f5f5f5 100%);background-image:-o-linear-gradient(top, #ebebeb 0, #f5f5f5 100%);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0, #ebebeb), to(#f5f5f5));background-image:linear-gradient(to bottom, #ebebeb 0, #f5f5f5 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0)}.progress-bar{background-image:-webkit-linear-gradient(top, #337ab7 0, #286090 100%);background-image:-o-linear-gradient(top, #337ab7 0, #286090 100%);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0, #337ab7), to(#286090));background-image:linear-gradient(to bottom, #337ab7 0, #286090 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff286090', GradientType=0)}.progress-bar-success{background-image:-webkit-linear-gradient(top, #5cb85c 0, #449d44 100%);background-image:-o-linear-gradient(top, #5cb85c 0, #449d44 100%);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0, #5cb85c), to(#449d44));background-image:linear-gradient(to bottom, #5cb85c 0, #449d44 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0)}.progress-bar-info{background-image:-webkit-linear-gradient(top, #5bc0de 0, #31b0d5 100%);background-image:-o-linear-gradient(top, #5bc0de 0, #31b0d5 100%);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0, #5bc0de), to(#31b0d5));background-image:linear-gradient(to bottom, #5bc0de 0, #31b0d5 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0)}.progress-bar-warning{background-image:-webkit-linear-gradient(top, #f0ad4e 0, #ec971f 100%);background-image:-o-linear-gradient(top, #f0ad4e 0, #ec971f 100%);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0, #f0ad4e), to(#ec971f));background-image:linear-gradient(to bottom, #f0ad4e 0, #ec971f 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0)}.progress-bar-danger{background-image:-webkit-linear-gradient(top, #d9534f 0, #c9302c 100%);background-image:-o-linear-gradient(top, #d9534f 0, #c9302c 100%);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0, #d9534f), to(#c9302c));background-image:linear-gradient(to bottom, #d9534f 0, #c9302c 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0)}.progress-bar-striped{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.list-group{border-radius:4px;-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.075);box-shadow:0 1px 2px rgba(0,0,0,0.075)}.list-group-item.active,.list-group-item.active:hover,.list-group-item.active:focus{text-shadow:0 -1px 0 #286090;background-image:-webkit-linear-gradient(top, #337ab7 0, #2b669a 100%);background-image:-o-linear-gradient(top, #337ab7 0, #2b669a 100%);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0, #337ab7), to(#2b669a));background-image:linear-gradient(to bottom, #337ab7 0, #2b669a 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2b669a', GradientType=0);border-color:#2b669a}.list-group-item.active .badge,.list-group-item.active:hover .badge,.list-group-item.active:focus .badge{text-shadow:none}.panel{-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.05);box-shadow:0 1px 2px rgba(0,0,0,0.05)}.panel-default>.panel-heading{background-image:-webkit-linear-gradient(top, #f5f5f5 0, #e8e8e8 100%);background-image:-o-linear-gradient(top, #f5f5f5 0, #e8e8e8 100%);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0, #f5f5f5), to(#e8e8e8));background-image:linear-gradient(to bottom, #f5f5f5 0, #e8e8e8 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0)}.panel-primary>.panel-heading{background-image:-webkit-linear-gradient(top, #337ab7 0, #2e6da4 100%);background-image:-o-linear-gradient(top, #337ab7 0, #2e6da4 100%);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0, #337ab7), to(#2e6da4));background-image:linear-gradient(to bottom, #337ab7 0, #2e6da4 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0)}.panel-success>.panel-heading{background-image:-webkit-linear-gradient(top, #dff0d8 0, #d0e9c6 100%);background-image:-o-linear-gradient(top, #dff0d8 0, #d0e9c6 100%);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0, #dff0d8), to(#d0e9c6));background-image:linear-gradient(to bottom, #dff0d8 0, #d0e9c6 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0)}.panel-info>.panel-heading{background-image:-webkit-linear-gradient(top, #d9edf7 0, #c4e3f3 100%);background-image:-o-linear-gradient(top, #d9edf7 0, #c4e3f3 100%);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0, #d9edf7), to(#c4e3f3));background-image:linear-gradient(to bottom, #d9edf7 0, #c4e3f3 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0)}.panel-warning>.panel-heading{background-image:-webkit-linear-gradient(top, #fcf8e3 0, #faf2cc 100%);background-image:-o-linear-gradient(top, #fcf8e3 0, #faf2cc 100%);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0, #fcf8e3), to(#faf2cc));background-image:linear-gradient(to bottom, #fcf8e3 0, #faf2cc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0)}.panel-danger>.panel-heading{background-image:-webkit-linear-gradient(top, #f2dede 0, #ebcccc 100%);background-image:-o-linear-gradient(top, #f2dede 0, #ebcccc 100%);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0, #f2dede), to(#ebcccc));background-image:linear-gradient(to bottom, #f2dede 0, #ebcccc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0)}.well{background-image:-webkit-linear-gradient(top, #e8e8e8 0, #f5f5f5 100%);background-image:-o-linear-gradient(top, #e8e8e8 0, #f5f5f5 100%);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0, #e8e8e8), to(#f5f5f5));background-image:linear-gradient(to bottom, #e8e8e8 0, #f5f5f5 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);border-color:#dcdcdc;-webkit-box-shadow:inset 0 1px 3px rgba(0,0,0,0.05),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 3px rgba(0,0,0,0.05),0 1px 0 rgba(255,255,255,0.1)} -------------------------------------------------------------------------------- /dist/watermark.js: -------------------------------------------------------------------------------- 1 | (function webpackUniversalModuleDefinition(root, factory) { 2 | if(typeof exports === 'object' && typeof module === 'object') 3 | module.exports = factory(); 4 | else if(typeof define === 'function' && define.amd) 5 | define([], factory); 6 | else if(typeof exports === 'object') 7 | exports["watermark"] = factory(); 8 | else 9 | root["watermark"] = factory(); 10 | })(window, function() { 11 | return /******/ (function(modules) { // webpackBootstrap 12 | /******/ // The module cache 13 | /******/ var installedModules = {}; 14 | /******/ 15 | /******/ // The require function 16 | /******/ function __webpack_require__(moduleId) { 17 | /******/ 18 | /******/ // Check if module is in cache 19 | /******/ if(installedModules[moduleId]) { 20 | /******/ return installedModules[moduleId].exports; 21 | /******/ } 22 | /******/ // Create a new module (and put it into the cache) 23 | /******/ var module = installedModules[moduleId] = { 24 | /******/ i: moduleId, 25 | /******/ l: false, 26 | /******/ exports: {} 27 | /******/ }; 28 | /******/ 29 | /******/ // Execute the module function 30 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 31 | /******/ 32 | /******/ // Flag the module as loaded 33 | /******/ module.l = true; 34 | /******/ 35 | /******/ // Return the exports of the module 36 | /******/ return module.exports; 37 | /******/ } 38 | /******/ 39 | /******/ 40 | /******/ // expose the modules object (__webpack_modules__) 41 | /******/ __webpack_require__.m = modules; 42 | /******/ 43 | /******/ // expose the module cache 44 | /******/ __webpack_require__.c = installedModules; 45 | /******/ 46 | /******/ // define getter function for harmony exports 47 | /******/ __webpack_require__.d = function(exports, name, getter) { 48 | /******/ if(!__webpack_require__.o(exports, name)) { 49 | /******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); 50 | /******/ } 51 | /******/ }; 52 | /******/ 53 | /******/ // define __esModule on exports 54 | /******/ __webpack_require__.r = function(exports) { 55 | /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { 56 | /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); 57 | /******/ } 58 | /******/ Object.defineProperty(exports, '__esModule', { value: true }); 59 | /******/ }; 60 | /******/ 61 | /******/ // create a fake namespace object 62 | /******/ // mode & 1: value is a module id, require it 63 | /******/ // mode & 2: merge all properties of value into the ns 64 | /******/ // mode & 4: return value when already ns object 65 | /******/ // mode & 8|1: behave like require 66 | /******/ __webpack_require__.t = function(value, mode) { 67 | /******/ if(mode & 1) value = __webpack_require__(value); 68 | /******/ if(mode & 8) return value; 69 | /******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; 70 | /******/ var ns = Object.create(null); 71 | /******/ __webpack_require__.r(ns); 72 | /******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }); 73 | /******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); 74 | /******/ return ns; 75 | /******/ }; 76 | /******/ 77 | /******/ // getDefaultExport function for compatibility with non-harmony modules 78 | /******/ __webpack_require__.n = function(module) { 79 | /******/ var getter = module && module.__esModule ? 80 | /******/ function getDefault() { return module['default']; } : 81 | /******/ function getModuleExports() { return module; }; 82 | /******/ __webpack_require__.d(getter, 'a', getter); 83 | /******/ return getter; 84 | /******/ }; 85 | /******/ 86 | /******/ // Object.prototype.hasOwnProperty.call 87 | /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; 88 | /******/ 89 | /******/ // __webpack_public_path__ 90 | /******/ __webpack_require__.p = ""; 91 | /******/ 92 | /******/ 93 | /******/ // Load entry module and return exports 94 | /******/ return __webpack_require__(__webpack_require__.s = 0); 95 | /******/ }) 96 | /************************************************************************/ 97 | /******/ ([ 98 | /* 0 */ 99 | /***/ (function(module, exports, __webpack_require__) { 100 | 101 | module.exports = __webpack_require__(1).default; 102 | 103 | 104 | /***/ }), 105 | /* 1 */ 106 | /***/ (function(module, __webpack_exports__, __webpack_require__) { 107 | 108 | "use strict"; 109 | __webpack_require__.r(__webpack_exports__); 110 | var style_image_namespaceObject = {}; 111 | __webpack_require__.r(style_image_namespaceObject); 112 | __webpack_require__.d(style_image_namespaceObject, "atPos", function() { return atPos; }); 113 | __webpack_require__.d(style_image_namespaceObject, "lowerRight", function() { return lowerRight; }); 114 | __webpack_require__.d(style_image_namespaceObject, "upperRight", function() { return upperRight; }); 115 | __webpack_require__.d(style_image_namespaceObject, "lowerLeft", function() { return lowerLeft; }); 116 | __webpack_require__.d(style_image_namespaceObject, "upperLeft", function() { return upperLeft; }); 117 | __webpack_require__.d(style_image_namespaceObject, "center", function() { return center; }); 118 | var text_namespaceObject = {}; 119 | __webpack_require__.r(text_namespaceObject); 120 | __webpack_require__.d(text_namespaceObject, "atPos", function() { return text_atPos; }); 121 | __webpack_require__.d(text_namespaceObject, "lowerRight", function() { return text_lowerRight; }); 122 | __webpack_require__.d(text_namespaceObject, "lowerLeft", function() { return text_lowerLeft; }); 123 | __webpack_require__.d(text_namespaceObject, "upperRight", function() { return text_upperRight; }); 124 | __webpack_require__.d(text_namespaceObject, "upperLeft", function() { return text_upperLeft; }); 125 | __webpack_require__.d(text_namespaceObject, "center", function() { return text_center; }); 126 | 127 | // CONCATENATED MODULE: ./lib/functions/index.js 128 | /** 129 | * Return a function that executes a sequence of functions from left to right, 130 | * passing the result of a previous operation to the next 131 | * 132 | * @param {...funcs} 133 | * @return {Function} 134 | */ 135 | function sequence() { 136 | for (var _len = arguments.length, funcs = new Array(_len), _key = 0; _key < _len; _key++) { 137 | funcs[_key] = arguments[_key]; 138 | } 139 | 140 | return function (value) { 141 | return funcs.reduce(function (val, fn) { 142 | return fn.call(null, val); 143 | }, value); 144 | }; 145 | } 146 | /** 147 | * Return the argument passed to it 148 | * 149 | * @param {Mixed} x 150 | * @return {Mixed} 151 | */ 152 | 153 | function identity(x) { 154 | return x; 155 | } 156 | // CONCATENATED MODULE: ./lib/image/index.js 157 | function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } 158 | 159 | 160 | /** 161 | * Set the src of an image object and call the resolve function 162 | * once it has loaded 163 | * 164 | * @param {Image} img 165 | * @param {String} src 166 | * @param {Function} resolve 167 | */ 168 | 169 | function setAndResolve(img, src, resolve) { 170 | img.onload = function () { 171 | return resolve(img); 172 | }; 173 | 174 | img.src = src; 175 | } 176 | /** 177 | * Given a resource, return an appropriate loading function for it's type 178 | * 179 | * @param {String|File|Image} resource 180 | * @return {Function} 181 | */ 182 | 183 | 184 | function getLoader(resource) { 185 | var type = _typeof(resource); 186 | 187 | if (type === 'string') { 188 | return loadUrl; 189 | } 190 | 191 | if (resource instanceof Image) { 192 | return identity; 193 | } 194 | 195 | return loadFile; 196 | } 197 | /** 198 | * Used for loading image resources asynchronously and maintaining 199 | * the supplied order of arguments 200 | * 201 | * @param {Array} resources - a mixed array of urls, File objects, or Image objects 202 | * @param {Function} init - called at the beginning of resource initialization 203 | * @return {Promise} 204 | */ 205 | 206 | function image_load(resources, init) { 207 | var promises = []; 208 | 209 | for (var i = 0; i < resources.length; i++) { 210 | var resource = resources[i]; 211 | var loader = getLoader(resource); 212 | var promise = loader(resource, init); 213 | promises.push(promise); 214 | } 215 | 216 | return Promise.all(promises); 217 | } 218 | /** 219 | * Load an image by its url 220 | * 221 | * @param {String} url 222 | * @param {Function} init - an optional image initializer 223 | * @return {Promise} 224 | */ 225 | 226 | function loadUrl(url, init) { 227 | var img = new Image(); 228 | typeof init === 'function' && init(img); 229 | return new Promise(function (resolve) { 230 | img.onload = function () { 231 | return resolve(img); 232 | }; 233 | 234 | img.src = url; 235 | }); 236 | } 237 | /** 238 | * Return a collection of images from an 239 | * array of File objects 240 | * 241 | * @param {File} file 242 | * @return {Promise} 243 | */ 244 | 245 | function loadFile(file) { 246 | var reader = new FileReader(); 247 | return new Promise(function (resolve) { 248 | var img = new Image(); 249 | 250 | reader.onloadend = function () { 251 | return setAndResolve(img, reader.result, resolve); 252 | }; 253 | 254 | reader.readAsDataURL(file); 255 | }); 256 | } 257 | /** 258 | * Create a new image, optionally configuring it's onload behavior 259 | * 260 | * @param {String} url 261 | * @param {Function} onload 262 | * @return {Image} 263 | */ 264 | 265 | function createImage(url, onload) { 266 | var img = new Image(); 267 | 268 | if (typeof onload === 'function') { 269 | img.onload = onload; 270 | } 271 | 272 | img.src = url; 273 | return img; 274 | } 275 | /** 276 | * Draw an image to a canvas element 277 | * 278 | * @param {Image} img 279 | * @param {HTMLCanvasElement} canvas 280 | * @return {HTMLCanvasElement} 281 | */ 282 | 283 | function drawImage(img, canvas) { 284 | var ctx = canvas.getContext('2d'); 285 | canvas.width = img.width; 286 | canvas.height = img.height; 287 | ctx.drawImage(img, 0, 0); 288 | return canvas; 289 | } 290 | /** 291 | * Convert an Image object to a canvas 292 | * 293 | * @param {Image} img 294 | * @param {CanvasPool} pool 295 | * @return {HTMLCanvasElement} 296 | */ 297 | 298 | 299 | function imageToCanvas(img, pool) { 300 | var canvas = pool.pop(); 301 | return drawImage(img, canvas); 302 | } 303 | /** 304 | * Convert an array of image objects 305 | * to canvas elements 306 | * 307 | * @param {Array} images 308 | * @param {CanvasPool} pool 309 | * @return {HTMLCanvasElement[]} 310 | */ 311 | 312 | function mapToCanvas(images, pool) { 313 | return images.map(function (img) { 314 | return imageToCanvas(img, pool); 315 | }); 316 | } 317 | // CONCATENATED MODULE: ./lib/canvas/index.js 318 | /** 319 | * Get the data url of a canvas 320 | * 321 | * @param {HTMLCanvasElement} 322 | * @param {Paramters} Specifications according to HTMLCanvasElement.toDataURL() Documentation 323 | * @return {String} 324 | */ 325 | function canvas_dataUrl(canvas) { 326 | var parameters = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : { 327 | type: 'image/png', 328 | encoderOptions: 0.92 329 | }; 330 | return canvas.toDataURL(parameters.type, parameters.encoderOptions); 331 | } 332 | // CONCATENATED MODULE: ./lib/blob/index.js 333 | 334 | var url = /^data:([^;]+);base64,(.*)$/; 335 | /** 336 | * Split a data url into a content type and raw data 337 | * 338 | * @param {String} dataUrl 339 | * @return {Array} 340 | */ 341 | 342 | function split(dataUrl) { 343 | return url.exec(dataUrl).slice(1); 344 | } 345 | /** 346 | * Decode a base64 string 347 | * 348 | * @param {String} base64 349 | * @return {String} 350 | */ 351 | 352 | function decode(base64) { 353 | return window.atob(base64); 354 | } 355 | /** 356 | * Return a string of raw data as a Uint8Array 357 | * 358 | * @param {String} data 359 | * @return {UInt8Array} 360 | */ 361 | 362 | function uint8(data) { 363 | var length = data.length; 364 | var uints = new Uint8Array(length); 365 | 366 | for (var i = 0; i < length; i++) { 367 | uints[i] = data.charCodeAt(i); 368 | } 369 | 370 | return uints; 371 | } 372 | /** 373 | * Turns a data url into a blob object 374 | * 375 | * @param {String} dataUrl 376 | * @return {Blob} 377 | */ 378 | 379 | var blob_blob = sequence(split, function (parts) { 380 | return [decode(parts[1]), parts[0]]; 381 | }, function (blob) { 382 | return new Blob([uint8(blob[0])], { 383 | type: blob[1] 384 | }); 385 | }); 386 | // CONCATENATED MODULE: ./lib/style/image/index.js 387 | /** 388 | * Return a function for positioning a watermark on a target canvas 389 | * 390 | * @param {Function} xFn - a function to determine an x value 391 | * @param {Function} yFn - a function to determine a y value 392 | * @param {Number} alpha 393 | * @return {Function} 394 | */ 395 | function atPos(xFn, yFn, alpha) { 396 | alpha || (alpha = 1.0); 397 | return function (target, watermark) { 398 | var context = target.getContext('2d'); 399 | context.save(); 400 | context.globalAlpha = alpha; 401 | context.drawImage(watermark, xFn(target, watermark), yFn(target, watermark)); 402 | context.restore(); 403 | return target; 404 | }; 405 | } 406 | /** 407 | * Place the watermark in the lower right corner of the target 408 | * image 409 | * 410 | * @param {Number} alpha 411 | * @return {Function} 412 | */ 413 | 414 | function lowerRight(alpha) { 415 | return atPos(function (target, mark) { 416 | return target.width - (mark.width + 10); 417 | }, function (target, mark) { 418 | return target.height - (mark.height + 10); 419 | }, alpha); 420 | } 421 | /** 422 | * Place the watermark in the upper right corner of the target 423 | * image 424 | * 425 | * @param {Number} alpha 426 | * @return {Function} 427 | */ 428 | 429 | function upperRight(alpha) { 430 | return atPos(function (target, mark) { 431 | return target.width - (mark.width + 10); 432 | }, function (target, mark) { 433 | return 10; 434 | }, alpha); 435 | } 436 | /** 437 | * Place the watermark in the lower left corner of the target 438 | * image 439 | * 440 | * @param {Number} alpha 441 | * @return {Function} 442 | */ 443 | 444 | function lowerLeft(alpha) { 445 | return atPos(function (target, mark) { 446 | return 10; 447 | }, function (target, mark) { 448 | return target.height - (mark.height + 10); 449 | }, alpha); 450 | } 451 | /** 452 | * Place the watermark in the upper left corner of the target 453 | * image 454 | * 455 | * @param {Number} alpha 456 | * @return {Function} 457 | */ 458 | 459 | function upperLeft(alpha) { 460 | return atPos(function (target, mark) { 461 | return 10; 462 | }, function (target, mark) { 463 | return 10; 464 | }, alpha); 465 | } 466 | /** 467 | * Place the watermark in the center of the target 468 | * image 469 | * 470 | * @param {Number} alpha 471 | * @return {Function} 472 | */ 473 | 474 | function center(alpha) { 475 | return atPos(function (target, mark) { 476 | return (target.width - mark.width) / 2; 477 | }, function (target, mark) { 478 | return (target.height - mark.height) / 2; 479 | }, alpha); 480 | } 481 | // CONCATENATED MODULE: ./lib/style/text/index.js 482 | /** 483 | * Return a function for positioning a watermark on a target canvas 484 | * 485 | * @param {Function} xFn - a function to determine an x value 486 | * @param {Function} yFn - a function to determine a y value 487 | * @param {String} text - the text to write 488 | * @param {String} font - same as the CSS font property 489 | * @param {String} fillStyle 490 | * @param {Number} alpha 491 | * @return {Function} 492 | */ 493 | function text_atPos(xFn, yFn, text, font, fillStyle, alpha) { 494 | alpha || (alpha = 1.0); 495 | return function (target) { 496 | var context = target.getContext('2d'); 497 | context.save(); 498 | context.globalAlpha = alpha; 499 | context.fillStyle = fillStyle; 500 | context.font = font; 501 | var metrics = context.measureText(text); 502 | context.fillText(text, xFn(target, metrics, context), yFn(target, metrics, context)); 503 | context.restore(); 504 | return target; 505 | }; 506 | } 507 | /** 508 | * Write text to the lower right corner of the target canvas 509 | * 510 | * @param {String} text - the text to write 511 | * @param {String} font - same as the CSS font property 512 | * @param {String} fillStyle 513 | * @param {Number} alpha - control text transparency 514 | * @param {Number} y - height in text metrics is not very well supported. This is a manual value 515 | * @return {Function} 516 | */ 517 | 518 | function text_lowerRight(text, font, fillStyle, alpha, y) { 519 | return text_atPos(function (target, metrics) { 520 | return target.width - (metrics.width + 10); 521 | }, function (target) { 522 | return y || target.height - 10; 523 | }, text, font, fillStyle, alpha); 524 | } 525 | /** 526 | * Write text to the lower left corner of the target canvas 527 | * 528 | * @param {String} text - the text to write 529 | * @param {String} font - same as the CSS font property 530 | * @param {String} fillStyle 531 | * @param {Number} alpha - control text transparency 532 | * @param {Number} y - height in text metrics is not very well supported. This is a manual value 533 | * @return {Function} 534 | */ 535 | 536 | function text_lowerLeft(text, font, fillStyle, alpha, y) { 537 | return text_atPos(function () { 538 | return 10; 539 | }, function (target) { 540 | return y || target.height - 10; 541 | }, text, font, fillStyle, alpha); 542 | } 543 | /** 544 | * Write text to the upper right corner of the target canvas 545 | * 546 | * @param {String} text - the text to write 547 | * @param {String} font - same as the CSS font property 548 | * @param {String} fillStyle 549 | * @param {Number} alpha - control text transparency 550 | * @param {Number} y - height in text metrics is not very well supported. This is a manual value 551 | * @return {Function} 552 | */ 553 | 554 | function text_upperRight(text, font, fillStyle, alpha, y) { 555 | return text_atPos(function (target, metrics) { 556 | return target.width - (metrics.width + 10); 557 | }, function () { 558 | return y || 20; 559 | }, text, font, fillStyle, alpha); 560 | } 561 | /** 562 | * Write text to the upper left corner of the target canvas 563 | * 564 | * @param {String} text - the text to write 565 | * @param {String} font - same as the CSS font property 566 | * @param {String} fillStyle 567 | * @param {Number} alpha - control text transparency 568 | * @param {Number} y - height in text metrics is not very well supported. This is a manual value 569 | * @return {Function} 570 | */ 571 | 572 | function text_upperLeft(text, font, fillStyle, alpha, y) { 573 | return text_atPos(function () { 574 | return 10; 575 | }, function () { 576 | return y || 20; 577 | }, text, font, fillStyle, alpha); 578 | } 579 | /** 580 | * Write text to the center of the target canvas 581 | * 582 | * @param {String} text - the text to write 583 | * @param {String} font - same as the CSS font property 584 | * @param {String} fillStyle 585 | * @param {Number} alpha - control text transparency 586 | * @param {Number} y - height in text metrics is not very well supported. This is a manual value 587 | * @return {Function} 588 | */ 589 | 590 | function text_center(text, font, fillStyle, alpha, y) { 591 | return text_atPos(function (target, metrics, ctx) { 592 | ctx.textAlign = 'center'; 593 | return target.width / 2; 594 | }, function (target, metrics, ctx) { 595 | ctx.textBaseline = 'middle'; 596 | return target.height / 2; 597 | }, text, font, fillStyle, alpha); 598 | } 599 | // CONCATENATED MODULE: ./lib/style/index.js 600 | 601 | 602 | /** 603 | * @typedef {Object} DrawResult 604 | * @property {HTMLCanvasElement} canvas - the end result of a draw 605 | * @property {HTMLCanvasElement[]} sources - the sources used in the draw 606 | */ 607 | 608 | var style_image = style_image_namespaceObject; 609 | var style_text = text_namespaceObject; 610 | /** 611 | * Create a DrawResult by apply a list of canvas elements to a draw function 612 | * 613 | * @param {Function} draw - the draw function used to create a DrawResult 614 | * @param {HTMLCanvasElement} sources - the canvases used by the draw function 615 | * @return {DrawResult} 616 | */ 617 | 618 | function style_result(draw, sources) { 619 | var canvas = draw.apply(null, sources); 620 | return { 621 | canvas: canvas, 622 | sources: sources 623 | }; 624 | } 625 | // CONCATENATED MODULE: ./lib/object/index.js 626 | /** 627 | * Extend one object with the properties of another 628 | * 629 | * @param {Object} first 630 | * @param {Object} second 631 | * @return {Object} 632 | */ 633 | function extend(first, second) { 634 | var secondKeys = Object.keys(second); 635 | secondKeys.forEach(function (key) { 636 | return first[key] = second[key]; 637 | }); 638 | return first; 639 | } 640 | /** 641 | * Create a shallow copy of the object 642 | * 643 | * @param {Object} obj 644 | * @return {Object} 645 | */ 646 | 647 | function clone(obj) { 648 | return extend({}, obj); 649 | } 650 | // CONCATENATED MODULE: ./lib/canvas/pool.js 651 | /** 652 | * An immutable canvas pool allowing more efficient use of canvas resources 653 | * 654 | * @typedef {Object} CanvasPool 655 | * @property {Function} pop - return a promise that will evaluate to a canvas 656 | * @property {Number} length - the number of available canvas elements 657 | * @property {HTMLCanvasElement[]} elements - the canvas elements used by the pool 658 | * @property {Function} clear - empty the pool of canvas elements 659 | * @property {Function} release - free a pool up for release and return the data url 660 | */ 661 | 662 | /** 663 | * Create a CanvasPool with the given size 664 | * 665 | * @param {Number} size 666 | * @param {HTMLCanvasElement[]} elements 667 | * @param {EventEmitter} eventEmitter 668 | * @return {CanvasPool} 669 | */ 670 | function CanvasPool() { 671 | var canvases = []; 672 | return { 673 | /** 674 | * Get the next available canvas from the pool 675 | * 676 | * @return {HTMLCanvasElement} 677 | */ 678 | pop: function pop() { 679 | if (this.length === 0) { 680 | canvases.push(document.createElement('canvas')); 681 | } 682 | 683 | return canvases.pop(); 684 | }, 685 | 686 | /** 687 | * Return the number of available canvas elements in the pool 688 | * 689 | * @return {Number} 690 | */ 691 | get length() { 692 | return canvases.length; 693 | }, 694 | 695 | /** 696 | * Return a canvas to the pool. This function will clear the canvas for reuse 697 | * 698 | * @param {HTMLCanvasElement} canvas 699 | * @return {String} 700 | */ 701 | release: function release(canvas) { 702 | var context = canvas.getContext('2d'); 703 | context.clearRect(0, 0, canvas.width, canvas.height); 704 | canvases.push(canvas); 705 | }, 706 | 707 | /** 708 | * Empty the pool, destroying any references to canvas objects 709 | */ 710 | clear: function clear() { 711 | canvases.splice(0, canvases.length); 712 | }, 713 | 714 | /** 715 | * Return the collection of canvases in the pool 716 | * 717 | * @return {HTMLCanvasElement[]} 718 | */ 719 | get elements() { 720 | return canvases; 721 | } 722 | 723 | }; 724 | } 725 | var shared = CanvasPool(); 726 | /* harmony default export */ var canvas_pool = (shared); 727 | // CONCATENATED MODULE: ./lib/index.js 728 | /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "default", function() { return watermark; }); 729 | 730 | 731 | 732 | 733 | 734 | 735 | /** 736 | * A configuration type for the watermark function 737 | * 738 | * @typedef {Object} Options 739 | * @property {Function} init - an initialization function that is given Image objects before loading (only applies if resources is a collection of urls) 740 | * @property {ImageFormat} type - specify the image format to be used when retrieving result (only supports "image/png" or "image/jpeg", default "image/png") 741 | * @property {Number} encoderOptions - specify the image compression quality from 0 to 1 (default 0.92) 742 | * @property {Number} poolSize - number of canvas elements available for drawing, 743 | * @property {CanvasPool} pool - the pool used. If provided, poolSize will be ignored 744 | */ 745 | 746 | /** 747 | * @constant 748 | * @type {Options} 749 | */ 750 | 751 | var defaults = { 752 | init: function init() {}, 753 | type: 'image/png', 754 | encoderOptions: 0.92 755 | }; 756 | /** 757 | * Merge the given options with the defaults 758 | * 759 | * @param {Options} options 760 | * @return {Options} 761 | */ 762 | 763 | function mergeOptions(options) { 764 | return extend(clone(defaults), options); 765 | } 766 | /** 767 | * Release canvases from a draw result for reuse. Returns 768 | * the dataURL from the result's canvas 769 | * 770 | * @param {DrawResult} result 771 | * @param {CanvasPool} pool 772 | * @return {String} 773 | */ 774 | 775 | 776 | function release(result, pool, parameters) { 777 | var canvas = result.canvas, 778 | sources = result.sources; 779 | var dataURL = canvas_dataUrl(canvas, parameters); 780 | sources.forEach(pool.release); 781 | return dataURL; 782 | } 783 | /** 784 | * Return a watermark object 785 | * 786 | * 787 | * @param {Array} resources - a collection of urls, File objects, or Image objects 788 | * @param {Options} options - a configuration object for watermark 789 | * @param {Promise} promise - optional 790 | * @return {Object} 791 | */ 792 | 793 | 794 | function watermark(resources) { 795 | var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 796 | var promise = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null; 797 | var opts = mergeOptions(options); 798 | promise || (promise = image_load(resources, opts.init)); 799 | return { 800 | /** 801 | * Convert the watermarked image into a dataUrl. The draw 802 | * function is given all images as canvas elements in order 803 | * 804 | * @param {Function} draw 805 | * @return {Object} 806 | */ 807 | dataUrl: function dataUrl(draw) { 808 | var promise = this.then(function (images) { 809 | return mapToCanvas(images, canvas_pool); 810 | }).then(function (canvases) { 811 | return style_result(draw, canvases); 812 | }).then(function (result) { 813 | return release(result, canvas_pool, { 814 | type: opts.type, 815 | encoderOptions: opts.encoderOptions 816 | }); 817 | }); 818 | return watermark(resources, opts, promise); 819 | }, 820 | 821 | /** 822 | * Load additional resources 823 | * 824 | * @param {Array} resources - a collection of urls, File objects, or Image objects 825 | * @param {Function} init - an initialization function that is given Image objects before loading (only applies if resources is a collection of urls) 826 | * @return {Object} 827 | */ 828 | load: function load(resources, init) { 829 | var promise = this.then(function (resource) { 830 | return image_load([resource].concat(resources), init); 831 | }); 832 | return watermark(resources, opts, promise); 833 | }, 834 | 835 | /** 836 | * Render the current state of the watermarked image. Useful for performing 837 | * actions after the watermark has been applied 838 | * 839 | * @return {Object} 840 | */ 841 | render: function render() { 842 | var promise = this.then(function (resource) { 843 | return image_load([resource]); 844 | }); 845 | return watermark(resources, opts, promise); 846 | }, 847 | 848 | /** 849 | * Convert the watermark into a blob 850 | * 851 | * @param {Function} draw 852 | * @return {Object} 853 | */ 854 | blob: function blob(draw) { 855 | var promise = this.dataUrl(draw).then(blob_blob); 856 | return watermark(resources, opts, promise); 857 | }, 858 | 859 | /** 860 | * Convert the watermark into an image using the given draw function 861 | * 862 | * @param {Function} draw 863 | * @return {Object} 864 | */ 865 | image: function image(draw) { 866 | var promise = this.dataUrl(draw).then(createImage); 867 | return watermark(resources, opts, promise); 868 | }, 869 | 870 | /** 871 | * Delegate to the watermark promise 872 | * 873 | * @return {Promise} 874 | */ 875 | then: function then() { 876 | for (var _len = arguments.length, funcs = new Array(_len), _key = 0; _key < _len; _key++) { 877 | funcs[_key] = arguments[_key]; 878 | } 879 | 880 | return promise.then.apply(promise, funcs); 881 | } 882 | }; 883 | } 884 | ; 885 | /** 886 | * Style functions 887 | */ 888 | 889 | watermark.image = style_image; 890 | watermark.text = style_text; 891 | /** 892 | * Clean up all canvas references 893 | */ 894 | 895 | watermark.destroy = function () { 896 | return canvas_pool.clear(); 897 | }; 898 | 899 | /***/ }) 900 | /******/ ]); 901 | }); -------------------------------------------------------------------------------- /examples/scripts/watermark.js: -------------------------------------------------------------------------------- 1 | (function webpackUniversalModuleDefinition(root, factory) { 2 | if(typeof exports === 'object' && typeof module === 'object') 3 | module.exports = factory(); 4 | else if(typeof define === 'function' && define.amd) 5 | define([], factory); 6 | else if(typeof exports === 'object') 7 | exports["watermark"] = factory(); 8 | else 9 | root["watermark"] = factory(); 10 | })(window, function() { 11 | return /******/ (function(modules) { // webpackBootstrap 12 | /******/ // The module cache 13 | /******/ var installedModules = {}; 14 | /******/ 15 | /******/ // The require function 16 | /******/ function __webpack_require__(moduleId) { 17 | /******/ 18 | /******/ // Check if module is in cache 19 | /******/ if(installedModules[moduleId]) { 20 | /******/ return installedModules[moduleId].exports; 21 | /******/ } 22 | /******/ // Create a new module (and put it into the cache) 23 | /******/ var module = installedModules[moduleId] = { 24 | /******/ i: moduleId, 25 | /******/ l: false, 26 | /******/ exports: {} 27 | /******/ }; 28 | /******/ 29 | /******/ // Execute the module function 30 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 31 | /******/ 32 | /******/ // Flag the module as loaded 33 | /******/ module.l = true; 34 | /******/ 35 | /******/ // Return the exports of the module 36 | /******/ return module.exports; 37 | /******/ } 38 | /******/ 39 | /******/ 40 | /******/ // expose the modules object (__webpack_modules__) 41 | /******/ __webpack_require__.m = modules; 42 | /******/ 43 | /******/ // expose the module cache 44 | /******/ __webpack_require__.c = installedModules; 45 | /******/ 46 | /******/ // define getter function for harmony exports 47 | /******/ __webpack_require__.d = function(exports, name, getter) { 48 | /******/ if(!__webpack_require__.o(exports, name)) { 49 | /******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); 50 | /******/ } 51 | /******/ }; 52 | /******/ 53 | /******/ // define __esModule on exports 54 | /******/ __webpack_require__.r = function(exports) { 55 | /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { 56 | /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); 57 | /******/ } 58 | /******/ Object.defineProperty(exports, '__esModule', { value: true }); 59 | /******/ }; 60 | /******/ 61 | /******/ // create a fake namespace object 62 | /******/ // mode & 1: value is a module id, require it 63 | /******/ // mode & 2: merge all properties of value into the ns 64 | /******/ // mode & 4: return value when already ns object 65 | /******/ // mode & 8|1: behave like require 66 | /******/ __webpack_require__.t = function(value, mode) { 67 | /******/ if(mode & 1) value = __webpack_require__(value); 68 | /******/ if(mode & 8) return value; 69 | /******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; 70 | /******/ var ns = Object.create(null); 71 | /******/ __webpack_require__.r(ns); 72 | /******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }); 73 | /******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); 74 | /******/ return ns; 75 | /******/ }; 76 | /******/ 77 | /******/ // getDefaultExport function for compatibility with non-harmony modules 78 | /******/ __webpack_require__.n = function(module) { 79 | /******/ var getter = module && module.__esModule ? 80 | /******/ function getDefault() { return module['default']; } : 81 | /******/ function getModuleExports() { return module; }; 82 | /******/ __webpack_require__.d(getter, 'a', getter); 83 | /******/ return getter; 84 | /******/ }; 85 | /******/ 86 | /******/ // Object.prototype.hasOwnProperty.call 87 | /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; 88 | /******/ 89 | /******/ // __webpack_public_path__ 90 | /******/ __webpack_require__.p = ""; 91 | /******/ 92 | /******/ 93 | /******/ // Load entry module and return exports 94 | /******/ return __webpack_require__(__webpack_require__.s = 0); 95 | /******/ }) 96 | /************************************************************************/ 97 | /******/ ([ 98 | /* 0 */ 99 | /***/ (function(module, exports, __webpack_require__) { 100 | 101 | module.exports = __webpack_require__(1).default; 102 | 103 | 104 | /***/ }), 105 | /* 1 */ 106 | /***/ (function(module, __webpack_exports__, __webpack_require__) { 107 | 108 | "use strict"; 109 | __webpack_require__.r(__webpack_exports__); 110 | var style_image_namespaceObject = {}; 111 | __webpack_require__.r(style_image_namespaceObject); 112 | __webpack_require__.d(style_image_namespaceObject, "atPos", function() { return atPos; }); 113 | __webpack_require__.d(style_image_namespaceObject, "lowerRight", function() { return lowerRight; }); 114 | __webpack_require__.d(style_image_namespaceObject, "upperRight", function() { return upperRight; }); 115 | __webpack_require__.d(style_image_namespaceObject, "lowerLeft", function() { return lowerLeft; }); 116 | __webpack_require__.d(style_image_namespaceObject, "upperLeft", function() { return upperLeft; }); 117 | __webpack_require__.d(style_image_namespaceObject, "center", function() { return center; }); 118 | var text_namespaceObject = {}; 119 | __webpack_require__.r(text_namespaceObject); 120 | __webpack_require__.d(text_namespaceObject, "atPos", function() { return text_atPos; }); 121 | __webpack_require__.d(text_namespaceObject, "lowerRight", function() { return text_lowerRight; }); 122 | __webpack_require__.d(text_namespaceObject, "lowerLeft", function() { return text_lowerLeft; }); 123 | __webpack_require__.d(text_namespaceObject, "upperRight", function() { return text_upperRight; }); 124 | __webpack_require__.d(text_namespaceObject, "upperLeft", function() { return text_upperLeft; }); 125 | __webpack_require__.d(text_namespaceObject, "center", function() { return text_center; }); 126 | 127 | // CONCATENATED MODULE: ./lib/functions/index.js 128 | /** 129 | * Return a function that executes a sequence of functions from left to right, 130 | * passing the result of a previous operation to the next 131 | * 132 | * @param {...funcs} 133 | * @return {Function} 134 | */ 135 | function sequence() { 136 | for (var _len = arguments.length, funcs = new Array(_len), _key = 0; _key < _len; _key++) { 137 | funcs[_key] = arguments[_key]; 138 | } 139 | 140 | return function (value) { 141 | return funcs.reduce(function (val, fn) { 142 | return fn.call(null, val); 143 | }, value); 144 | }; 145 | } 146 | /** 147 | * Return the argument passed to it 148 | * 149 | * @param {Mixed} x 150 | * @return {Mixed} 151 | */ 152 | 153 | function identity(x) { 154 | return x; 155 | } 156 | // CONCATENATED MODULE: ./lib/image/index.js 157 | function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } 158 | 159 | 160 | /** 161 | * Set the src of an image object and call the resolve function 162 | * once it has loaded 163 | * 164 | * @param {Image} img 165 | * @param {String} src 166 | * @param {Function} resolve 167 | */ 168 | 169 | function setAndResolve(img, src, resolve) { 170 | img.onload = function () { 171 | return resolve(img); 172 | }; 173 | 174 | img.src = src; 175 | } 176 | /** 177 | * Given a resource, return an appropriate loading function for it's type 178 | * 179 | * @param {String|File|Image} resource 180 | * @return {Function} 181 | */ 182 | 183 | 184 | function getLoader(resource) { 185 | var type = _typeof(resource); 186 | 187 | if (type === 'string') { 188 | return loadUrl; 189 | } 190 | 191 | if (resource instanceof Image) { 192 | return identity; 193 | } 194 | 195 | return loadFile; 196 | } 197 | /** 198 | * Used for loading image resources asynchronously and maintaining 199 | * the supplied order of arguments 200 | * 201 | * @param {Array} resources - a mixed array of urls, File objects, or Image objects 202 | * @param {Function} init - called at the beginning of resource initialization 203 | * @return {Promise} 204 | */ 205 | 206 | function image_load(resources, init) { 207 | var promises = []; 208 | 209 | for (var i = 0; i < resources.length; i++) { 210 | var resource = resources[i]; 211 | var loader = getLoader(resource); 212 | var promise = loader(resource, init); 213 | promises.push(promise); 214 | } 215 | 216 | return Promise.all(promises); 217 | } 218 | /** 219 | * Load an image by its url 220 | * 221 | * @param {String} url 222 | * @param {Function} init - an optional image initializer 223 | * @return {Promise} 224 | */ 225 | 226 | function loadUrl(url, init) { 227 | var img = new Image(); 228 | typeof init === 'function' && init(img); 229 | return new Promise(function (resolve) { 230 | img.onload = function () { 231 | return resolve(img); 232 | }; 233 | 234 | img.src = url; 235 | }); 236 | } 237 | /** 238 | * Return a collection of images from an 239 | * array of File objects 240 | * 241 | * @param {File} file 242 | * @return {Promise} 243 | */ 244 | 245 | function loadFile(file) { 246 | var reader = new FileReader(); 247 | return new Promise(function (resolve) { 248 | var img = new Image(); 249 | 250 | reader.onloadend = function () { 251 | return setAndResolve(img, reader.result, resolve); 252 | }; 253 | 254 | reader.readAsDataURL(file); 255 | }); 256 | } 257 | /** 258 | * Create a new image, optionally configuring it's onload behavior 259 | * 260 | * @param {String} url 261 | * @param {Function} onload 262 | * @return {Image} 263 | */ 264 | 265 | function createImage(url, onload) { 266 | var img = new Image(); 267 | 268 | if (typeof onload === 'function') { 269 | img.onload = onload; 270 | } 271 | 272 | img.src = url; 273 | return img; 274 | } 275 | /** 276 | * Draw an image to a canvas element 277 | * 278 | * @param {Image} img 279 | * @param {HTMLCanvasElement} canvas 280 | * @return {HTMLCanvasElement} 281 | */ 282 | 283 | function drawImage(img, canvas) { 284 | var ctx = canvas.getContext('2d'); 285 | canvas.width = img.width; 286 | canvas.height = img.height; 287 | ctx.drawImage(img, 0, 0); 288 | return canvas; 289 | } 290 | /** 291 | * Convert an Image object to a canvas 292 | * 293 | * @param {Image} img 294 | * @param {CanvasPool} pool 295 | * @return {HTMLCanvasElement} 296 | */ 297 | 298 | 299 | function imageToCanvas(img, pool) { 300 | var canvas = pool.pop(); 301 | return drawImage(img, canvas); 302 | } 303 | /** 304 | * Convert an array of image objects 305 | * to canvas elements 306 | * 307 | * @param {Array} images 308 | * @param {CanvasPool} pool 309 | * @return {HTMLCanvasElement[]} 310 | */ 311 | 312 | function mapToCanvas(images, pool) { 313 | return images.map(function (img) { 314 | return imageToCanvas(img, pool); 315 | }); 316 | } 317 | // CONCATENATED MODULE: ./lib/canvas/index.js 318 | /** 319 | * Get the data url of a canvas 320 | * 321 | * @param {HTMLCanvasElement} 322 | * @param {Paramters} Specifications according to HTMLCanvasElement.toDataURL() Documentation 323 | * @return {String} 324 | */ 325 | function canvas_dataUrl(canvas) { 326 | var parameters = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : { 327 | type: 'image/png', 328 | encoderOptions: 0.92 329 | }; 330 | return canvas.toDataURL(parameters.type, parameters.encoderOptions); 331 | } 332 | // CONCATENATED MODULE: ./lib/blob/index.js 333 | 334 | var url = /^data:([^;]+);base64,(.*)$/; 335 | /** 336 | * Split a data url into a content type and raw data 337 | * 338 | * @param {String} dataUrl 339 | * @return {Array} 340 | */ 341 | 342 | function split(dataUrl) { 343 | return url.exec(dataUrl).slice(1); 344 | } 345 | /** 346 | * Decode a base64 string 347 | * 348 | * @param {String} base64 349 | * @return {String} 350 | */ 351 | 352 | function decode(base64) { 353 | return window.atob(base64); 354 | } 355 | /** 356 | * Return a string of raw data as a Uint8Array 357 | * 358 | * @param {String} data 359 | * @return {UInt8Array} 360 | */ 361 | 362 | function uint8(data) { 363 | var length = data.length; 364 | var uints = new Uint8Array(length); 365 | 366 | for (var i = 0; i < length; i++) { 367 | uints[i] = data.charCodeAt(i); 368 | } 369 | 370 | return uints; 371 | } 372 | /** 373 | * Turns a data url into a blob object 374 | * 375 | * @param {String} dataUrl 376 | * @return {Blob} 377 | */ 378 | 379 | var blob_blob = sequence(split, function (parts) { 380 | return [decode(parts[1]), parts[0]]; 381 | }, function (blob) { 382 | return new Blob([uint8(blob[0])], { 383 | type: blob[1] 384 | }); 385 | }); 386 | // CONCATENATED MODULE: ./lib/style/image/index.js 387 | /** 388 | * Return a function for positioning a watermark on a target canvas 389 | * 390 | * @param {Function} xFn - a function to determine an x value 391 | * @param {Function} yFn - a function to determine a y value 392 | * @param {Number} alpha 393 | * @return {Function} 394 | */ 395 | function atPos(xFn, yFn, alpha) { 396 | alpha || (alpha = 1.0); 397 | return function (target, watermark) { 398 | var context = target.getContext('2d'); 399 | context.save(); 400 | context.globalAlpha = alpha; 401 | context.drawImage(watermark, xFn(target, watermark), yFn(target, watermark)); 402 | context.restore(); 403 | return target; 404 | }; 405 | } 406 | /** 407 | * Place the watermark in the lower right corner of the target 408 | * image 409 | * 410 | * @param {Number} alpha 411 | * @return {Function} 412 | */ 413 | 414 | function lowerRight(alpha) { 415 | return atPos(function (target, mark) { 416 | return target.width - (mark.width + 10); 417 | }, function (target, mark) { 418 | return target.height - (mark.height + 10); 419 | }, alpha); 420 | } 421 | /** 422 | * Place the watermark in the upper right corner of the target 423 | * image 424 | * 425 | * @param {Number} alpha 426 | * @return {Function} 427 | */ 428 | 429 | function upperRight(alpha) { 430 | return atPos(function (target, mark) { 431 | return target.width - (mark.width + 10); 432 | }, function (target, mark) { 433 | return 10; 434 | }, alpha); 435 | } 436 | /** 437 | * Place the watermark in the lower left corner of the target 438 | * image 439 | * 440 | * @param {Number} alpha 441 | * @return {Function} 442 | */ 443 | 444 | function lowerLeft(alpha) { 445 | return atPos(function (target, mark) { 446 | return 10; 447 | }, function (target, mark) { 448 | return target.height - (mark.height + 10); 449 | }, alpha); 450 | } 451 | /** 452 | * Place the watermark in the upper left corner of the target 453 | * image 454 | * 455 | * @param {Number} alpha 456 | * @return {Function} 457 | */ 458 | 459 | function upperLeft(alpha) { 460 | return atPos(function (target, mark) { 461 | return 10; 462 | }, function (target, mark) { 463 | return 10; 464 | }, alpha); 465 | } 466 | /** 467 | * Place the watermark in the center of the target 468 | * image 469 | * 470 | * @param {Number} alpha 471 | * @return {Function} 472 | */ 473 | 474 | function center(alpha) { 475 | return atPos(function (target, mark) { 476 | return (target.width - mark.width) / 2; 477 | }, function (target, mark) { 478 | return (target.height - mark.height) / 2; 479 | }, alpha); 480 | } 481 | // CONCATENATED MODULE: ./lib/style/text/index.js 482 | /** 483 | * Return a function for positioning a watermark on a target canvas 484 | * 485 | * @param {Function} xFn - a function to determine an x value 486 | * @param {Function} yFn - a function to determine a y value 487 | * @param {String} text - the text to write 488 | * @param {String} font - same as the CSS font property 489 | * @param {String} fillStyle 490 | * @param {Number} alpha 491 | * @return {Function} 492 | */ 493 | function text_atPos(xFn, yFn, text, font, fillStyle, alpha) { 494 | alpha || (alpha = 1.0); 495 | return function (target) { 496 | var context = target.getContext('2d'); 497 | context.save(); 498 | context.globalAlpha = alpha; 499 | context.fillStyle = fillStyle; 500 | context.font = font; 501 | var metrics = context.measureText(text); 502 | context.fillText(text, xFn(target, metrics, context), yFn(target, metrics, context)); 503 | context.restore(); 504 | return target; 505 | }; 506 | } 507 | /** 508 | * Write text to the lower right corner of the target canvas 509 | * 510 | * @param {String} text - the text to write 511 | * @param {String} font - same as the CSS font property 512 | * @param {String} fillStyle 513 | * @param {Number} alpha - control text transparency 514 | * @param {Number} y - height in text metrics is not very well supported. This is a manual value 515 | * @return {Function} 516 | */ 517 | 518 | function text_lowerRight(text, font, fillStyle, alpha, y) { 519 | return text_atPos(function (target, metrics) { 520 | return target.width - (metrics.width + 10); 521 | }, function (target) { 522 | return y || target.height - 10; 523 | }, text, font, fillStyle, alpha); 524 | } 525 | /** 526 | * Write text to the lower left corner of the target canvas 527 | * 528 | * @param {String} text - the text to write 529 | * @param {String} font - same as the CSS font property 530 | * @param {String} fillStyle 531 | * @param {Number} alpha - control text transparency 532 | * @param {Number} y - height in text metrics is not very well supported. This is a manual value 533 | * @return {Function} 534 | */ 535 | 536 | function text_lowerLeft(text, font, fillStyle, alpha, y) { 537 | return text_atPos(function () { 538 | return 10; 539 | }, function (target) { 540 | return y || target.height - 10; 541 | }, text, font, fillStyle, alpha); 542 | } 543 | /** 544 | * Write text to the upper right corner of the target canvas 545 | * 546 | * @param {String} text - the text to write 547 | * @param {String} font - same as the CSS font property 548 | * @param {String} fillStyle 549 | * @param {Number} alpha - control text transparency 550 | * @param {Number} y - height in text metrics is not very well supported. This is a manual value 551 | * @return {Function} 552 | */ 553 | 554 | function text_upperRight(text, font, fillStyle, alpha, y) { 555 | return text_atPos(function (target, metrics) { 556 | return target.width - (metrics.width + 10); 557 | }, function () { 558 | return y || 20; 559 | }, text, font, fillStyle, alpha); 560 | } 561 | /** 562 | * Write text to the upper left corner of the target canvas 563 | * 564 | * @param {String} text - the text to write 565 | * @param {String} font - same as the CSS font property 566 | * @param {String} fillStyle 567 | * @param {Number} alpha - control text transparency 568 | * @param {Number} y - height in text metrics is not very well supported. This is a manual value 569 | * @return {Function} 570 | */ 571 | 572 | function text_upperLeft(text, font, fillStyle, alpha, y) { 573 | return text_atPos(function () { 574 | return 10; 575 | }, function () { 576 | return y || 20; 577 | }, text, font, fillStyle, alpha); 578 | } 579 | /** 580 | * Write text to the center of the target canvas 581 | * 582 | * @param {String} text - the text to write 583 | * @param {String} font - same as the CSS font property 584 | * @param {String} fillStyle 585 | * @param {Number} alpha - control text transparency 586 | * @param {Number} y - height in text metrics is not very well supported. This is a manual value 587 | * @return {Function} 588 | */ 589 | 590 | function text_center(text, font, fillStyle, alpha, y) { 591 | return text_atPos(function (target, metrics, ctx) { 592 | ctx.textAlign = 'center'; 593 | return target.width / 2; 594 | }, function (target, metrics, ctx) { 595 | ctx.textBaseline = 'middle'; 596 | return target.height / 2; 597 | }, text, font, fillStyle, alpha); 598 | } 599 | // CONCATENATED MODULE: ./lib/style/index.js 600 | 601 | 602 | /** 603 | * @typedef {Object} DrawResult 604 | * @property {HTMLCanvasElement} canvas - the end result of a draw 605 | * @property {HTMLCanvasElement[]} sources - the sources used in the draw 606 | */ 607 | 608 | var style_image = style_image_namespaceObject; 609 | var style_text = text_namespaceObject; 610 | /** 611 | * Create a DrawResult by apply a list of canvas elements to a draw function 612 | * 613 | * @param {Function} draw - the draw function used to create a DrawResult 614 | * @param {HTMLCanvasElement} sources - the canvases used by the draw function 615 | * @return {DrawResult} 616 | */ 617 | 618 | function style_result(draw, sources) { 619 | var canvas = draw.apply(null, sources); 620 | return { 621 | canvas: canvas, 622 | sources: sources 623 | }; 624 | } 625 | // CONCATENATED MODULE: ./lib/object/index.js 626 | /** 627 | * Extend one object with the properties of another 628 | * 629 | * @param {Object} first 630 | * @param {Object} second 631 | * @return {Object} 632 | */ 633 | function extend(first, second) { 634 | var secondKeys = Object.keys(second); 635 | secondKeys.forEach(function (key) { 636 | return first[key] = second[key]; 637 | }); 638 | return first; 639 | } 640 | /** 641 | * Create a shallow copy of the object 642 | * 643 | * @param {Object} obj 644 | * @return {Object} 645 | */ 646 | 647 | function clone(obj) { 648 | return extend({}, obj); 649 | } 650 | // CONCATENATED MODULE: ./lib/canvas/pool.js 651 | /** 652 | * An immutable canvas pool allowing more efficient use of canvas resources 653 | * 654 | * @typedef {Object} CanvasPool 655 | * @property {Function} pop - return a promise that will evaluate to a canvas 656 | * @property {Number} length - the number of available canvas elements 657 | * @property {HTMLCanvasElement[]} elements - the canvas elements used by the pool 658 | * @property {Function} clear - empty the pool of canvas elements 659 | * @property {Function} release - free a pool up for release and return the data url 660 | */ 661 | 662 | /** 663 | * Create a CanvasPool with the given size 664 | * 665 | * @param {Number} size 666 | * @param {HTMLCanvasElement[]} elements 667 | * @param {EventEmitter} eventEmitter 668 | * @return {CanvasPool} 669 | */ 670 | function CanvasPool() { 671 | var canvases = []; 672 | return { 673 | /** 674 | * Get the next available canvas from the pool 675 | * 676 | * @return {HTMLCanvasElement} 677 | */ 678 | pop: function pop() { 679 | if (this.length === 0) { 680 | canvases.push(document.createElement('canvas')); 681 | } 682 | 683 | return canvases.pop(); 684 | }, 685 | 686 | /** 687 | * Return the number of available canvas elements in the pool 688 | * 689 | * @return {Number} 690 | */ 691 | get length() { 692 | return canvases.length; 693 | }, 694 | 695 | /** 696 | * Return a canvas to the pool. This function will clear the canvas for reuse 697 | * 698 | * @param {HTMLCanvasElement} canvas 699 | * @return {String} 700 | */ 701 | release: function release(canvas) { 702 | var context = canvas.getContext('2d'); 703 | context.clearRect(0, 0, canvas.width, canvas.height); 704 | canvases.push(canvas); 705 | }, 706 | 707 | /** 708 | * Empty the pool, destroying any references to canvas objects 709 | */ 710 | clear: function clear() { 711 | canvases.splice(0, canvases.length); 712 | }, 713 | 714 | /** 715 | * Return the collection of canvases in the pool 716 | * 717 | * @return {HTMLCanvasElement[]} 718 | */ 719 | get elements() { 720 | return canvases; 721 | } 722 | 723 | }; 724 | } 725 | var shared = CanvasPool(); 726 | /* harmony default export */ var canvas_pool = (shared); 727 | // CONCATENATED MODULE: ./lib/index.js 728 | /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "default", function() { return watermark; }); 729 | 730 | 731 | 732 | 733 | 734 | 735 | /** 736 | * A configuration type for the watermark function 737 | * 738 | * @typedef {Object} Options 739 | * @property {Function} init - an initialization function that is given Image objects before loading (only applies if resources is a collection of urls) 740 | * @property {ImageFormat} type - specify the image format to be used when retrieving result (only supports "image/png" or "image/jpeg", default "image/png") 741 | * @property {Number} encoderOptions - specify the image compression quality from 0 to 1 (default 0.92) 742 | * @property {Number} poolSize - number of canvas elements available for drawing, 743 | * @property {CanvasPool} pool - the pool used. If provided, poolSize will be ignored 744 | */ 745 | 746 | /** 747 | * @constant 748 | * @type {Options} 749 | */ 750 | 751 | var defaults = { 752 | init: function init() {}, 753 | type: 'image/png', 754 | encoderOptions: 0.92 755 | }; 756 | /** 757 | * Merge the given options with the defaults 758 | * 759 | * @param {Options} options 760 | * @return {Options} 761 | */ 762 | 763 | function mergeOptions(options) { 764 | return extend(clone(defaults), options); 765 | } 766 | /** 767 | * Release canvases from a draw result for reuse. Returns 768 | * the dataURL from the result's canvas 769 | * 770 | * @param {DrawResult} result 771 | * @param {CanvasPool} pool 772 | * @return {String} 773 | */ 774 | 775 | 776 | function release(result, pool, parameters) { 777 | var canvas = result.canvas, 778 | sources = result.sources; 779 | var dataURL = canvas_dataUrl(canvas, parameters); 780 | sources.forEach(pool.release); 781 | return dataURL; 782 | } 783 | /** 784 | * Return a watermark object 785 | * 786 | * 787 | * @param {Array} resources - a collection of urls, File objects, or Image objects 788 | * @param {Options} options - a configuration object for watermark 789 | * @param {Promise} promise - optional 790 | * @return {Object} 791 | */ 792 | 793 | 794 | function watermark(resources) { 795 | var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 796 | var promise = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null; 797 | var opts = mergeOptions(options); 798 | promise || (promise = image_load(resources, opts.init)); 799 | return { 800 | /** 801 | * Convert the watermarked image into a dataUrl. The draw 802 | * function is given all images as canvas elements in order 803 | * 804 | * @param {Function} draw 805 | * @return {Object} 806 | */ 807 | dataUrl: function dataUrl(draw) { 808 | var promise = this.then(function (images) { 809 | return mapToCanvas(images, canvas_pool); 810 | }).then(function (canvases) { 811 | return style_result(draw, canvases); 812 | }).then(function (result) { 813 | return release(result, canvas_pool, { 814 | type: opts.type, 815 | encoderOptions: opts.encoderOptions 816 | }); 817 | }); 818 | return watermark(resources, opts, promise); 819 | }, 820 | 821 | /** 822 | * Load additional resources 823 | * 824 | * @param {Array} resources - a collection of urls, File objects, or Image objects 825 | * @param {Function} init - an initialization function that is given Image objects before loading (only applies if resources is a collection of urls) 826 | * @return {Object} 827 | */ 828 | load: function load(resources, init) { 829 | var promise = this.then(function (resource) { 830 | return image_load([resource].concat(resources), init); 831 | }); 832 | return watermark(resources, opts, promise); 833 | }, 834 | 835 | /** 836 | * Render the current state of the watermarked image. Useful for performing 837 | * actions after the watermark has been applied 838 | * 839 | * @return {Object} 840 | */ 841 | render: function render() { 842 | var promise = this.then(function (resource) { 843 | return image_load([resource]); 844 | }); 845 | return watermark(resources, opts, promise); 846 | }, 847 | 848 | /** 849 | * Convert the watermark into a blob 850 | * 851 | * @param {Function} draw 852 | * @return {Object} 853 | */ 854 | blob: function blob(draw) { 855 | var promise = this.dataUrl(draw).then(blob_blob); 856 | return watermark(resources, opts, promise); 857 | }, 858 | 859 | /** 860 | * Convert the watermark into an image using the given draw function 861 | * 862 | * @param {Function} draw 863 | * @return {Object} 864 | */ 865 | image: function image(draw) { 866 | var promise = this.dataUrl(draw).then(createImage); 867 | return watermark(resources, opts, promise); 868 | }, 869 | 870 | /** 871 | * Delegate to the watermark promise 872 | * 873 | * @return {Promise} 874 | */ 875 | then: function then() { 876 | for (var _len = arguments.length, funcs = new Array(_len), _key = 0; _key < _len; _key++) { 877 | funcs[_key] = arguments[_key]; 878 | } 879 | 880 | return promise.then.apply(promise, funcs); 881 | } 882 | }; 883 | } 884 | ; 885 | /** 886 | * Style functions 887 | */ 888 | 889 | watermark.image = style_image; 890 | watermark.text = style_text; 891 | /** 892 | * Clean up all canvas references 893 | */ 894 | 895 | watermark.destroy = function () { 896 | return canvas_pool.clear(); 897 | }; 898 | 899 | /***/ }) 900 | /******/ ]); 901 | }); --------------------------------------------------------------------------------