[
  {
    "path": ".github/workflows/actions.yml",
    "content": "name: picasso-canvas-fingerprinting\non: [push]\njobs:\n  check-picasso-consistency:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n      - uses: actions/setup-node@v3\n        with:\n          node-version: '17'\n      - run: npm install\n      - run: npm run test"
  },
  {
    "path": ".gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n.pnpm-debug.log*\n\n# Diagnostic reports (https://nodejs.org/api/report.html)\nreport.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json\n\n# Runtime data\npids\n*.pid\n*.seed\n*.pid.lock\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\n*.lcov\n\n# nyc test coverage\n.nyc_output\n\n# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)\n.grunt\n\n# Bower dependency directory (https://bower.io/)\nbower_components\n\n# node-waf configuration\n.lock-wscript\n\n# Compiled binary addons (https://nodejs.org/api/addons.html)\nbuild/Release\n\n# Dependency directories\nnode_modules/\njspm_packages/\n\n# Snowpack dependency directory (https://snowpack.dev/)\nweb_modules/\n\n# TypeScript cache\n*.tsbuildinfo\n\n# Optional npm cache directory\n.npm\n\n# Optional eslint cache\n.eslintcache\n\n# Optional stylelint cache\n.stylelintcache\n\n# Microbundle cache\n.rpt2_cache/\n.rts2_cache_cjs/\n.rts2_cache_es/\n.rts2_cache_umd/\n\n# Optional REPL history\n.node_repl_history\n\n# Output of 'npm pack'\n*.tgz\n\n# Yarn Integrity file\n.yarn-integrity\n\n# dotenv environment variable files\n.env\n.env.development.local\n.env.test.local\n.env.production.local\n.env.local\n\n# parcel-bundler cache (https://parceljs.org/)\n.cache\n.parcel-cache\n\n# Next.js build output\n.next\nout\n\n# Nuxt.js build / generate output\n.nuxt\ndist\n\n# Gatsby files\n.cache/\n# Comment in the public line in if your project uses Gatsby and not Next.js\n# https://nextjs.org/blog/next-9-1#public-directory-support\n# public\n\n# vuepress build output\n.vuepress/dist\n\n# vuepress v2.x temp and cache directory\n.temp\n.cache\n\n# Docusaurus cache and generated files\n.docusaurus\n\n# Serverless directories\n.serverless/\n\n# FuseBox cache\n.fusebox/\n\n# DynamoDB Local files\n.dynamodb/\n\n# TernJS port file\n.tern-port\n\n# Stores VSCode versions used for testing VSCode extensions\n.vscode-test\n\n# yarn v2\n.yarn/cache\n.yarn/unplugged\n.yarn/build-state.yml\n.yarn/install-state.gz\n.pnp.*\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2019 antoine vastel\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# Picasso based canvas fingerprinting\n\n[![Actions Status](https://github.com/antoinevastel/picasso-like-canvas-fingerprinting//workflows/picasso-canvas-fingerprinting/badge.svg)](https://github.com/antoinevastel/picasso-like-canvas-fingerprinting/actions)\n[![NPM package](https://img.shields.io/npm/v/picasso-canvas-fingerprinting.svg)](https://www.npmjs.com/package/picasso-canvas-fingerprinting)\n\n\n## News:\nI recently created a [website where you can see your browser fingerprint](https://deviceandbrowserinfo.com/info_device) and different fingerprinting-related signals like your IP address, your canvas fingerprint, your HTTP headers, etc. Some information is accessible both through a webpage and through APIs. Don't hesitate to bookmark it as I will add more signals and more content related to bots.\n\n------------\n\n\nImplementation of a canvas fingerprinting algorithm inspired by the [Picasso paper](https://ai.google/research/pubs/pub45581) written by Elie Bursztein.\n\nAn online demo is available on [my blog](https://antoinevastel.com/browser%20fingerprinting/2019/03/21/picasso-canvas-fingerprinting.html).\n\n## Quick start\n\n```html\nYou can host your own version of the Picasso canvas fingerprinting script or include it using Jsdelivr CDN.\n<script src=\"https://cdn.jsdelivr.net/npm/picasso-canvas-fingerprinting/src/canvas.js\"></script>\n```\n\nOnce the Picasso script is loaded in your HTML page, you can use it as follows:\n```html\n<script>\n    const params = {\n        area: {\n            width: 300,\n            height: 300,\n        },\n        offsetParameter: 2001000001,\n        fontSizeFactor: 1.5,\n        multiplier: 15000,\n        maxShadowBlur: 50,\n    };\n\n    // Number of shapes to draw. The higher the more costly it is.\n    // Can be used as a way to adjust the aggressiveness of the proof of work (POW)\n    const numShapes = 5;\n    const initialSeed = Math.floor(100*Math.random());\n\n    const canvasValue = picassoCanvas(\n        numShapes, initialSeed, params\n    );\n\n    // canvasValue is a hash representing the result of the Picasso challenge, e.g.\n    // c24b4a72badc95284b337aa304be1438\n</script>\n```\n\n"
  },
  {
    "path": "examples/demo.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n<style>\n</style>\n<script src=\"canvas.js\"></script>\n\n<body>\n    <script>\n        const params = {\n            area: {\n                width: 300,\n                height: 300,\n            },\n            offsetParameter: 2001000001,\n            fontSizeFactor: 1.5,\n            multiplier: 15000,\n            maxShadowBlur: 50,\n        };\n        const numShapes = 5;\n        const initialSeed = Math.floor(100 * Math.random());\n\n        const canvasValue = picassoCanvas(\n            numShapes, initialSeed, params\n        );\n        console.log(`Picasso hash = ${canvasValue}`);\n    </script>\n</body>\n\n</html>"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"picasso-canvas-fingerprinting\",\n  \"version\": \"1.0.1\",\n  \"description\": \"Implementation of a canvas fingerprinting algorithm inspired by the Picasso paper\",\n  \"main\": \"src/canvas.js\",\n  \"scripts\": {\n    \"test\": \"node_modules/mocha/bin/mocha.js\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/antoinevastel/picasso-like-canvas-fingerprinting.git\"\n  },\n  \"author\": \"Antoine Vastel\",\n  \"license\": \"MIT\",\n  \"bugs\": {\n    \"url\": \"https://github.com/antoinevastel/picasso-like-canvas-fingerprinting/issues\"\n  },\n  \"homepage\": \"https://github.com/antoinevastel/picasso-like-canvas-fingerprinting#readme\",\n  \"devDependencies\": {\n    \"mocha\": \"^10.0.0\",\n    \"puppeteer\": \"^15.4.0\"\n  }\n}\n"
  },
  {
    "path": "src/canvas.js",
    "content": "function x64Add(m, n) {\n    m = [m[0] >>> 16, m[0] & 0xffff, m[1] >>> 16, m[1] & 0xffff];\n    n = [n[0] >>> 16, n[0] & 0xffff, n[1] >>> 16, n[1] & 0xffff];\n    var o = [0, 0, 0, 0];\n    o[3] += m[3] + n[3];\n    o[2] += o[3] >>> 16;\n    o[3] &= 0xffff;\n    o[2] += m[2] + n[2];\n    o[1] += o[2] >>> 16;\n    o[2] &= 0xffff;\n    o[1] += m[1] + n[1];\n    o[0] += o[1] >>> 16;\n    o[1] &= 0xffff;\n    o[0] += m[0] + n[0];\n    o[0] &= 0xffff;\n    return [(o[0] << 16) | o[1], (o[2] << 16) | o[3]];\n}\n\n\nfunction x64Multiply(m, n) {\n    m = [m[0] >>> 16, m[0] & 0xffff, m[1] >>> 16, m[1] & 0xffff];\n    n = [n[0] >>> 16, n[0] & 0xffff, n[1] >>> 16, n[1] & 0xffff];\n    var o = [0, 0, 0, 0];\n    o[3] += m[3] * n[3];\n    o[2] += o[3] >>> 16;\n    o[3] &= 0xffff;\n    o[2] += m[2] * n[3];\n    o[1] += o[2] >>> 16;\n    o[2] &= 0xffff;\n    o[2] += m[3] * n[2];\n    o[1] += o[2] >>> 16;\n    o[2] &= 0xffff;\n    o[1] += m[1] * n[3];\n    o[0] += o[1] >>> 16;\n    o[1] &= 0xffff;\n    o[1] += m[2] * n[2];\n    o[0] += o[1] >>> 16;\n    o[1] &= 0xffff;\n    o[1] += m[3] * n[1];\n    o[0] += o[1] >>> 16;\n    o[1] &= 0xffff;\n    o[0] += (m[0] * n[3]) + (m[1] * n[2]) + (m[2] * n[1]) + (m[3] * n[0]);\n    o[0] &= 0xffff;\n    return [(o[0] << 16) | o[1], (o[2] << 16) | o[3]];\n}\n\nfunction x64Rotl(m, n) {\n    n %= 64;\n    if (n === 32) {\n        return [m[1], m[0]]\n    } else if (n < 32) {\n        return [(m[0] << n) | (m[1] >>> (32 - n)), (m[1] << n) | (m[0] >>> (32 - n))]\n    } else {\n        n -= 32;\n        return [(m[1] << n) | (m[0] >>> (32 - n)), (m[0] << n) | (m[1] >>> (32 - n))]\n    }\n}\n\nfunction x64LeftShift(m, n) {\n    n %= 64;\n    if (n === 0) {\n        return m\n    } else if (n < 32) {\n        return [(m[0] << n) | (m[1] >>> (32 - n)), m[1] << n]\n    } else {\n        return [m[1] << (n - 32), 0]\n    }\n}\n\nfunction x64Xor(m, n) {\n    return [m[0] ^ n[0], m[1] ^ n[1]];\n}\n\nfunction x64Fmix(h) {\n    h = x64Xor(h, [0, h[0] >>> 1]);\n    h = x64Multiply(h, [0xff51afd7, 0xed558ccd]);\n    h = x64Xor(h, [0, h[0] >>> 1]);\n    h = x64Multiply(h, [0xc4ceb9fe, 0x1a85ec53]);\n    h = x64Xor(h, [0, h[0] >>> 1]);\n    return h\n}\n\nfunction x64hash128(key, seed) {\n    key = key || '';\n    seed = seed || 0;\n    var remainder = key.length % 16;\n    var bytes = key.length - remainder;\n    var h1 = [0, seed];\n    var h2 = [0, seed];\n    var k1 = [0, 0];\n    var k2 = [0, 0];\n    var c1 = [0x87c37b91, 0x114253d5];\n    var c2 = [0x4cf5ad43, 0x2745937f];\n    for (var i = 0; i < bytes; i = i + 16) {\n        k1 = [((key.charCodeAt(i + 4) & 0xff)) | ((key.charCodeAt(i + 5) & 0xff) << 8) | ((key.charCodeAt(i + 6) & 0xff) << 16) | ((key.charCodeAt(i + 7) & 0xff) << 24), ((key.charCodeAt(i) & 0xff)) | ((key.charCodeAt(i + 1) & 0xff) << 8) | ((key.charCodeAt(i + 2) & 0xff) << 16) | ((key.charCodeAt(i + 3) & 0xff) << 24)];\n        k2 = [((key.charCodeAt(i + 12) & 0xff)) | ((key.charCodeAt(i + 13) & 0xff) << 8) | ((key.charCodeAt(i + 14) & 0xff) << 16) | ((key.charCodeAt(i + 15) & 0xff) << 24), ((key.charCodeAt(i + 8) & 0xff)) | ((key.charCodeAt(i + 9) & 0xff) << 8) | ((key.charCodeAt(i + 10) & 0xff) << 16) | ((key.charCodeAt(i + 11) & 0xff) << 24)];\n        k1 = x64Multiply(k1, c1);\n        k1 = x64Rotl(k1, 31);\n        k1 = x64Multiply(k1, c2);\n        h1 = x64Xor(h1, k1);\n        h1 = x64Rotl(h1, 27);\n        h1 = x64Add(h1, h2);\n        h1 = x64Add(x64Multiply(h1, [0, 5]), [0, 0x52dce729]);\n        k2 = x64Multiply(k2, c2);\n        k2 = x64Rotl(k2, 33);\n        k2 = x64Multiply(k2, c1);\n        h2 = x64Xor(h2, k2);\n        h2 = x64Rotl(h2, 31);\n        h2 = x64Add(h2, h1);\n        h2 = x64Add(x64Multiply(h2, [0, 5]), [0, 0x38495ab5]);\n    }\n    k1 = [0, 0];\n    k2 = [0, 0];\n    switch (remainder) {\n        case 15:\n            k2 = x64Xor(k2, x64LeftShift([0, key.charCodeAt(i + 14)], 48));\n        case 14:\n            k2 = x64Xor(k2, x64LeftShift([0, key.charCodeAt(i + 13)], 40));\n        case 13:\n            k2 = x64Xor(k2, x64LeftShift([0, key.charCodeAt(i + 12)], 32));\n        case 12:\n            k2 = x64Xor(k2, x64LeftShift([0, key.charCodeAt(i + 11)], 24));\n        case 11:\n            k2 = x64Xor(k2, x64LeftShift([0, key.charCodeAt(i + 10)], 16));\n        case 10:\n            k2 = x64Xor(k2, x64LeftShift([0, key.charCodeAt(i + 9)], 8));\n        case 9:\n            k2 = x64Xor(k2, [0, key.charCodeAt(i + 8)]);\n            k2 = x64Multiply(k2, c2);\n            k2 = x64Rotl(k2, 33);\n            k2 = x64Multiply(k2, c1);\n            h2 = x64Xor(h2, k2);\n        case 8:\n            k1 = x64Xor(k1, x64LeftShift([0, key.charCodeAt(i + 7)], 56));\n        case 7:\n            k1 = x64Xor(k1, x64LeftShift([0, key.charCodeAt(i + 6)], 48));\n        case 6:\n            k1 = x64Xor(k1, x64LeftShift([0, key.charCodeAt(i + 5)], 40));\n        case 5:\n            k1 = x64Xor(k1, x64LeftShift([0, key.charCodeAt(i + 4)], 32));\n        case 4:\n            k1 = x64Xor(k1, x64LeftShift([0, key.charCodeAt(i + 3)], 24));\n        case 3:\n            k1 = x64Xor(k1, x64LeftShift([0, key.charCodeAt(i + 2)], 16));\n        case 2:\n            k1 = x64Xor(k1, x64LeftShift([0, key.charCodeAt(i + 1)], 8));\n        case 1:\n            k1 = x64Xor(k1, [0, key.charCodeAt(i)]);\n            k1 = x64Multiply(k1, c1);\n            k1 = x64Rotl(k1, 31);\n            k1 = x64Multiply(k1, c2);\n            h1 = x64Xor(h1, k1);\n    }\n    h1 = x64Xor(h1, [0, key.length]);\n    h2 = x64Xor(h2, [0, key.length]);\n    h1 = x64Add(h1, h2);\n    h2 = x64Add(h2, h1);\n    h1 = x64Fmix(h1);\n    h2 = x64Fmix(h2);\n    h1 = x64Add(h1, h2);\n    h2 = x64Add(h2, h1);\n    return ('00000000' + (h1[0] >>> 0).toString(16)).slice(-8) + ('00000000' + (h1[1] >>> 0).toString(16)).slice(-8) + ('00000000' + (h2[0] >>> 0).toString(16)).slice(-8) + ('00000000' + (h2[1] >>> 0).toString(16)).slice(-8)\n}\n\nfunction picassoCanvas(roundNumber, seed, params) {\n    const {area, offsetParameter, multiplier, fontSizeFactor, maxShadowBlur} = params;\n\n    class Prng {\n        constructor(seed) {\n            this.currentNumber = seed % offsetParameter;\n            if (this.currentNumber <= 0) {\n                this.currentNumber += offsetParameter\n            }\n        }\n\n        getNext() {\n            this.currentNumber = multiplier * this.currentNumber % offsetParameter;\n            return this.currentNumber;\n        }\n    }\n\n    function adaptRandomNumberToContext(randomNumber, maxBound, floatAllowed) {\n        randomNumber = (randomNumber - 1) / offsetParameter;\n        if (floatAllowed) {\n            return randomNumber * maxBound;\n        }\n\n        return Math.floor(randomNumber * maxBound);\n    }\n\n    function addRandomCanvasGradient(prng, context, area) {\n        const canvasGradient = context.createRadialGradient(\n            adaptRandomNumberToContext(prng.getNext(), area.width),\n            adaptRandomNumberToContext(prng.getNext(), area.height),\n            adaptRandomNumberToContext(prng.getNext(), area.width),\n            adaptRandomNumberToContext(prng.getNext(), area.width),\n            adaptRandomNumberToContext(prng.getNext(), area.height),\n            adaptRandomNumberToContext(prng.getNext(), area.width)\n        );\n        canvasGradient.addColorStop(0, colors[adaptRandomNumberToContext(prng.getNext(), colors.length)]);\n        canvasGradient.addColorStop(1, colors[adaptRandomNumberToContext(prng.getNext(), colors.length)]);\n        context.fillStyle = canvasGradient\n    }\n\n    function generateRandomWord(prng, wordLength) {\n        const minAscii = 65;\n        const maxAscii = 126;\n        const wordGenerated = [];\n        for (let i = 0; i < wordLength; i++) {\n            const asciiCode = minAscii + (prng.getNext() % (maxAscii - minAscii));\n            wordGenerated.push(String.fromCharCode(asciiCode));\n        }\n\n        return wordGenerated.join('');\n    }\n\n    if (!window.CanvasRenderingContext2D) {\n        return 'unknown';\n    }\n\n    const colors = ['#FF6633', '#FFB399', '#FF33FF', '#FFFF99', '#00B3E6',\n        '#E6B333', '#3366E6', '#999966', '#99FF99', '#B34D4D',\n        '#80B300', '#809900', '#E6B3B3', '#6680B3', '#66991A',\n        '#FF99E6', '#CCFF1A', '#FF1A66', '#E6331A', '#33FFCC',\n        '#66994D', '#B366CC', '#4D8000', '#B33300', '#CC80CC',\n        '#66664D', '#991AFF', '#E666FF', '#4DB3FF', '#1AB399',\n        '#E666B3', '#33991A', '#CC9999', '#B3B31A', '#00E680',\n        '#4D8066', '#809980', '#E6FF80', '#1AFF33', '#999933',\n        '#FF3380', '#CCCC00', '#66E64D', '#4D80CC', '#9900B3',\n        '#E64D66', '#4DB380', '#FF4D4D', '#99E6E6', '#6666FF'];\n\n    const primitives = [\n        function arc (prng, context, area) {\n            context.beginPath();\n            context.arc(\n                adaptRandomNumberToContext(prng.getNext(), area.width),\n                adaptRandomNumberToContext(prng.getNext(), area.height),\n                adaptRandomNumberToContext(prng.getNext(), Math.min(area.width, area.height)),\n                adaptRandomNumberToContext(prng.getNext(), 2 * Math.PI, true),\n                adaptRandomNumberToContext(prng.getNext(), 2 * Math.PI, true)\n            );\n            context.stroke()\n        },\n        function text (prng, context, area) {\n            const wordLength = Math.max(1, adaptRandomNumberToContext(prng.getNext(), 5));\n            const textToStroke = generateRandomWord(prng, wordLength);\n            context.font = `${area.height / fontSizeFactor}px aafakefontaa`;\n\n            context.strokeText(\n              textToStroke,\n              adaptRandomNumberToContext(prng.getNext(), area.width),\n              adaptRandomNumberToContext(prng.getNext(), area.height),\n              adaptRandomNumberToContext(prng.getNext(), area.width)\n            )\n        },\n        function bezierCurve (prng, context, area) {\n            context.beginPath();\n            context.moveTo(\n                adaptRandomNumberToContext(prng.getNext(), area.width),\n                adaptRandomNumberToContext(prng.getNext(), area.height)\n            );\n            context.bezierCurveTo(\n                adaptRandomNumberToContext(prng.getNext(), area.width),\n                adaptRandomNumberToContext(prng.getNext(), area.height),\n                adaptRandomNumberToContext(prng.getNext(), area.width),\n                adaptRandomNumberToContext(prng.getNext(), area.height),\n                adaptRandomNumberToContext(prng.getNext(), area.width),\n                adaptRandomNumberToContext(prng.getNext(), area.height)\n            );\n            context.stroke()\n        },\n        function quadraticCurve(prng, context, area) {\n            context.beginPath();\n            context.moveTo(\n                adaptRandomNumberToContext(prng.getNext(), area.width),\n                adaptRandomNumberToContext(prng.getNext(), area.height)\n            );\n            context.quadraticCurveTo(\n                adaptRandomNumberToContext(prng.getNext(), area.width),\n                adaptRandomNumberToContext(prng.getNext(), area.height),\n                adaptRandomNumberToContext(prng.getNext(), area.width),\n                adaptRandomNumberToContext(prng.getNext(), area.height)\n            );\n            context.stroke()\n        },\n        function ellipse(prng, context, area) {\n            context.beginPath();\n            context.ellipse(\n                adaptRandomNumberToContext(prng.getNext(), area.width),\n                adaptRandomNumberToContext(prng.getNext(), area.height),\n                adaptRandomNumberToContext(prng.getNext(), Math.floor(area.width/2)),\n                adaptRandomNumberToContext(prng.getNext(), Math.floor(area.height/2)),\n                adaptRandomNumberToContext(prng.getNext(), 2 * Math.PI, true),\n                adaptRandomNumberToContext(prng.getNext(), 2 * Math.PI, true),\n                adaptRandomNumberToContext(prng.getNext(), 2 * Math.PI, true)\n            );\n\n            context.stroke()\n        }\n    ];\n\n    try {\n        const prng = new Prng(seed);\n        const canvasElt = document.createElement(\"canvas\");\n        canvasElt.width = area.width;\n        canvasElt.height = area.height;\n        canvasElt.style.display = \"none\";\n        const context = canvasElt.getContext(\"2d\");\n        for (let i = 0; i < roundNumber; i++) {\n            addRandomCanvasGradient(prng, context, area);\n            context.shadowBlur = adaptRandomNumberToContext(prng.getNext(), maxShadowBlur);\n            context.shadowColor = colors[adaptRandomNumberToContext(prng.getNext(), colors.length)];\n            const randomPrimitive = primitives[adaptRandomNumberToContext(prng.getNext(), primitives.length)];\n            randomPrimitive(prng, context, area);\n            context.fill()\n        }\n\n        return x64hash128(canvasElt.toDataURL(), seed);\n    } catch (e) {}\n}\n"
  },
  {
    "path": "test/test.js",
    "content": "const assert = require('assert');\nconst puppeteer = require('puppeteer');\nconst fs = require('fs');\n\ndescribe('Picasso canvas fingerprinting', function() {\n    this.timeout(10000);\n    it('Picasso value should be consistent', async () => {\n        const browser = await puppeteer.launch();\n        const page = await browser.newPage();\n        const picassoCode = fs.readFileSync('./src/canvas.js', 'utf8');\n        await page.addScriptTag({content: picassoCode});\n\n        const res = await page.evaluate(() => {\n            const params = {\n                area: {\n                    width: 300,\n                    height: 300,\n                },\n                offsetParameter: 2001000001,\n                fontSizeFactor: 1.5,\n                multiplier: 15000,\n                maxShadowBlur: 50,\n            };\n            const numShapes = 5;\n            const initialSeed = Math.floor(100*Math.random());\n\n            const canvasValue = picassoCanvas(\n                numShapes, initialSeed, params\n            );\n\n            return canvasValue;\n        })\n\n        await browser.close();\n        assert.equal(typeof res, 'string');\n        assert.equal(res.length > 15, true);\n    });\n});"
  }
]