Repository: antoinevastel/picasso-like-canvas-fingerprinting
Branch: master
Commit: a39549806072
Files: 8
Total size: 20.4 KB
Directory structure:
gitextract_wymqla_6/
├── .github/
│ └── workflows/
│ └── actions.yml
├── .gitignore
├── LICENSE
├── README.md
├── examples/
│ └── demo.html
├── package.json
├── src/
│ └── canvas.js
└── test/
└── test.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/actions.yml
================================================
name: picasso-canvas-fingerprinting
on: [push]
jobs:
check-picasso-consistency:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '17'
- run: npm install
- run: npm run test
================================================
FILE: .gitignore
================================================
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2019 antoine vastel
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# Picasso based canvas fingerprinting
[](https://github.com/antoinevastel/picasso-like-canvas-fingerprinting/actions)
[](https://www.npmjs.com/package/picasso-canvas-fingerprinting)
## News:
I 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.
------------
Implementation of a canvas fingerprinting algorithm inspired by the [Picasso paper](https://ai.google/research/pubs/pub45581) written by Elie Bursztein.
An online demo is available on [my blog](https://antoinevastel.com/browser%20fingerprinting/2019/03/21/picasso-canvas-fingerprinting.html).
## Quick start
```html
You can host your own version of the Picasso canvas fingerprinting script or include it using Jsdelivr CDN.
<script src="https://cdn.jsdelivr.net/npm/picasso-canvas-fingerprinting/src/canvas.js"></script>
```
Once the Picasso script is loaded in your HTML page, you can use it as follows:
```html
<script>
const params = {
area: {
width: 300,
height: 300,
},
offsetParameter: 2001000001,
fontSizeFactor: 1.5,
multiplier: 15000,
maxShadowBlur: 50,
};
// Number of shapes to draw. The higher the more costly it is.
// Can be used as a way to adjust the aggressiveness of the proof of work (POW)
const numShapes = 5;
const initialSeed = Math.floor(100*Math.random());
const canvasValue = picassoCanvas(
numShapes, initialSeed, params
);
// canvasValue is a hash representing the result of the Picasso challenge, e.g.
// c24b4a72badc95284b337aa304be1438
</script>
```
================================================
FILE: examples/demo.html
================================================
<!DOCTYPE html>
<html lang="en">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<style>
</style>
<script src="canvas.js"></script>
<body>
<script>
const params = {
area: {
width: 300,
height: 300,
},
offsetParameter: 2001000001,
fontSizeFactor: 1.5,
multiplier: 15000,
maxShadowBlur: 50,
};
const numShapes = 5;
const initialSeed = Math.floor(100 * Math.random());
const canvasValue = picassoCanvas(
numShapes, initialSeed, params
);
console.log(`Picasso hash = ${canvasValue}`);
</script>
</body>
</html>
================================================
FILE: package.json
================================================
{
"name": "picasso-canvas-fingerprinting",
"version": "1.0.1",
"description": "Implementation of a canvas fingerprinting algorithm inspired by the Picasso paper",
"main": "src/canvas.js",
"scripts": {
"test": "node_modules/mocha/bin/mocha.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/antoinevastel/picasso-like-canvas-fingerprinting.git"
},
"author": "Antoine Vastel",
"license": "MIT",
"bugs": {
"url": "https://github.com/antoinevastel/picasso-like-canvas-fingerprinting/issues"
},
"homepage": "https://github.com/antoinevastel/picasso-like-canvas-fingerprinting#readme",
"devDependencies": {
"mocha": "^10.0.0",
"puppeteer": "^15.4.0"
}
}
================================================
FILE: src/canvas.js
================================================
function x64Add(m, n) {
m = [m[0] >>> 16, m[0] & 0xffff, m[1] >>> 16, m[1] & 0xffff];
n = [n[0] >>> 16, n[0] & 0xffff, n[1] >>> 16, n[1] & 0xffff];
var o = [0, 0, 0, 0];
o[3] += m[3] + n[3];
o[2] += o[3] >>> 16;
o[3] &= 0xffff;
o[2] += m[2] + n[2];
o[1] += o[2] >>> 16;
o[2] &= 0xffff;
o[1] += m[1] + n[1];
o[0] += o[1] >>> 16;
o[1] &= 0xffff;
o[0] += m[0] + n[0];
o[0] &= 0xffff;
return [(o[0] << 16) | o[1], (o[2] << 16) | o[3]];
}
function x64Multiply(m, n) {
m = [m[0] >>> 16, m[0] & 0xffff, m[1] >>> 16, m[1] & 0xffff];
n = [n[0] >>> 16, n[0] & 0xffff, n[1] >>> 16, n[1] & 0xffff];
var o = [0, 0, 0, 0];
o[3] += m[3] * n[3];
o[2] += o[3] >>> 16;
o[3] &= 0xffff;
o[2] += m[2] * n[3];
o[1] += o[2] >>> 16;
o[2] &= 0xffff;
o[2] += m[3] * n[2];
o[1] += o[2] >>> 16;
o[2] &= 0xffff;
o[1] += m[1] * n[3];
o[0] += o[1] >>> 16;
o[1] &= 0xffff;
o[1] += m[2] * n[2];
o[0] += o[1] >>> 16;
o[1] &= 0xffff;
o[1] += m[3] * n[1];
o[0] += o[1] >>> 16;
o[1] &= 0xffff;
o[0] += (m[0] * n[3]) + (m[1] * n[2]) + (m[2] * n[1]) + (m[3] * n[0]);
o[0] &= 0xffff;
return [(o[0] << 16) | o[1], (o[2] << 16) | o[3]];
}
function x64Rotl(m, n) {
n %= 64;
if (n === 32) {
return [m[1], m[0]]
} else if (n < 32) {
return [(m[0] << n) | (m[1] >>> (32 - n)), (m[1] << n) | (m[0] >>> (32 - n))]
} else {
n -= 32;
return [(m[1] << n) | (m[0] >>> (32 - n)), (m[0] << n) | (m[1] >>> (32 - n))]
}
}
function x64LeftShift(m, n) {
n %= 64;
if (n === 0) {
return m
} else if (n < 32) {
return [(m[0] << n) | (m[1] >>> (32 - n)), m[1] << n]
} else {
return [m[1] << (n - 32), 0]
}
}
function x64Xor(m, n) {
return [m[0] ^ n[0], m[1] ^ n[1]];
}
function x64Fmix(h) {
h = x64Xor(h, [0, h[0] >>> 1]);
h = x64Multiply(h, [0xff51afd7, 0xed558ccd]);
h = x64Xor(h, [0, h[0] >>> 1]);
h = x64Multiply(h, [0xc4ceb9fe, 0x1a85ec53]);
h = x64Xor(h, [0, h[0] >>> 1]);
return h
}
function x64hash128(key, seed) {
key = key || '';
seed = seed || 0;
var remainder = key.length % 16;
var bytes = key.length - remainder;
var h1 = [0, seed];
var h2 = [0, seed];
var k1 = [0, 0];
var k2 = [0, 0];
var c1 = [0x87c37b91, 0x114253d5];
var c2 = [0x4cf5ad43, 0x2745937f];
for (var i = 0; i < bytes; i = i + 16) {
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)];
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)];
k1 = x64Multiply(k1, c1);
k1 = x64Rotl(k1, 31);
k1 = x64Multiply(k1, c2);
h1 = x64Xor(h1, k1);
h1 = x64Rotl(h1, 27);
h1 = x64Add(h1, h2);
h1 = x64Add(x64Multiply(h1, [0, 5]), [0, 0x52dce729]);
k2 = x64Multiply(k2, c2);
k2 = x64Rotl(k2, 33);
k2 = x64Multiply(k2, c1);
h2 = x64Xor(h2, k2);
h2 = x64Rotl(h2, 31);
h2 = x64Add(h2, h1);
h2 = x64Add(x64Multiply(h2, [0, 5]), [0, 0x38495ab5]);
}
k1 = [0, 0];
k2 = [0, 0];
switch (remainder) {
case 15:
k2 = x64Xor(k2, x64LeftShift([0, key.charCodeAt(i + 14)], 48));
case 14:
k2 = x64Xor(k2, x64LeftShift([0, key.charCodeAt(i + 13)], 40));
case 13:
k2 = x64Xor(k2, x64LeftShift([0, key.charCodeAt(i + 12)], 32));
case 12:
k2 = x64Xor(k2, x64LeftShift([0, key.charCodeAt(i + 11)], 24));
case 11:
k2 = x64Xor(k2, x64LeftShift([0, key.charCodeAt(i + 10)], 16));
case 10:
k2 = x64Xor(k2, x64LeftShift([0, key.charCodeAt(i + 9)], 8));
case 9:
k2 = x64Xor(k2, [0, key.charCodeAt(i + 8)]);
k2 = x64Multiply(k2, c2);
k2 = x64Rotl(k2, 33);
k2 = x64Multiply(k2, c1);
h2 = x64Xor(h2, k2);
case 8:
k1 = x64Xor(k1, x64LeftShift([0, key.charCodeAt(i + 7)], 56));
case 7:
k1 = x64Xor(k1, x64LeftShift([0, key.charCodeAt(i + 6)], 48));
case 6:
k1 = x64Xor(k1, x64LeftShift([0, key.charCodeAt(i + 5)], 40));
case 5:
k1 = x64Xor(k1, x64LeftShift([0, key.charCodeAt(i + 4)], 32));
case 4:
k1 = x64Xor(k1, x64LeftShift([0, key.charCodeAt(i + 3)], 24));
case 3:
k1 = x64Xor(k1, x64LeftShift([0, key.charCodeAt(i + 2)], 16));
case 2:
k1 = x64Xor(k1, x64LeftShift([0, key.charCodeAt(i + 1)], 8));
case 1:
k1 = x64Xor(k1, [0, key.charCodeAt(i)]);
k1 = x64Multiply(k1, c1);
k1 = x64Rotl(k1, 31);
k1 = x64Multiply(k1, c2);
h1 = x64Xor(h1, k1);
}
h1 = x64Xor(h1, [0, key.length]);
h2 = x64Xor(h2, [0, key.length]);
h1 = x64Add(h1, h2);
h2 = x64Add(h2, h1);
h1 = x64Fmix(h1);
h2 = x64Fmix(h2);
h1 = x64Add(h1, h2);
h2 = x64Add(h2, h1);
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)
}
function picassoCanvas(roundNumber, seed, params) {
const {area, offsetParameter, multiplier, fontSizeFactor, maxShadowBlur} = params;
class Prng {
constructor(seed) {
this.currentNumber = seed % offsetParameter;
if (this.currentNumber <= 0) {
this.currentNumber += offsetParameter
}
}
getNext() {
this.currentNumber = multiplier * this.currentNumber % offsetParameter;
return this.currentNumber;
}
}
function adaptRandomNumberToContext(randomNumber, maxBound, floatAllowed) {
randomNumber = (randomNumber - 1) / offsetParameter;
if (floatAllowed) {
return randomNumber * maxBound;
}
return Math.floor(randomNumber * maxBound);
}
function addRandomCanvasGradient(prng, context, area) {
const canvasGradient = context.createRadialGradient(
adaptRandomNumberToContext(prng.getNext(), area.width),
adaptRandomNumberToContext(prng.getNext(), area.height),
adaptRandomNumberToContext(prng.getNext(), area.width),
adaptRandomNumberToContext(prng.getNext(), area.width),
adaptRandomNumberToContext(prng.getNext(), area.height),
adaptRandomNumberToContext(prng.getNext(), area.width)
);
canvasGradient.addColorStop(0, colors[adaptRandomNumberToContext(prng.getNext(), colors.length)]);
canvasGradient.addColorStop(1, colors[adaptRandomNumberToContext(prng.getNext(), colors.length)]);
context.fillStyle = canvasGradient
}
function generateRandomWord(prng, wordLength) {
const minAscii = 65;
const maxAscii = 126;
const wordGenerated = [];
for (let i = 0; i < wordLength; i++) {
const asciiCode = minAscii + (prng.getNext() % (maxAscii - minAscii));
wordGenerated.push(String.fromCharCode(asciiCode));
}
return wordGenerated.join('');
}
if (!window.CanvasRenderingContext2D) {
return 'unknown';
}
const colors = ['#FF6633', '#FFB399', '#FF33FF', '#FFFF99', '#00B3E6',
'#E6B333', '#3366E6', '#999966', '#99FF99', '#B34D4D',
'#80B300', '#809900', '#E6B3B3', '#6680B3', '#66991A',
'#FF99E6', '#CCFF1A', '#FF1A66', '#E6331A', '#33FFCC',
'#66994D', '#B366CC', '#4D8000', '#B33300', '#CC80CC',
'#66664D', '#991AFF', '#E666FF', '#4DB3FF', '#1AB399',
'#E666B3', '#33991A', '#CC9999', '#B3B31A', '#00E680',
'#4D8066', '#809980', '#E6FF80', '#1AFF33', '#999933',
'#FF3380', '#CCCC00', '#66E64D', '#4D80CC', '#9900B3',
'#E64D66', '#4DB380', '#FF4D4D', '#99E6E6', '#6666FF'];
const primitives = [
function arc (prng, context, area) {
context.beginPath();
context.arc(
adaptRandomNumberToContext(prng.getNext(), area.width),
adaptRandomNumberToContext(prng.getNext(), area.height),
adaptRandomNumberToContext(prng.getNext(), Math.min(area.width, area.height)),
adaptRandomNumberToContext(prng.getNext(), 2 * Math.PI, true),
adaptRandomNumberToContext(prng.getNext(), 2 * Math.PI, true)
);
context.stroke()
},
function text (prng, context, area) {
const wordLength = Math.max(1, adaptRandomNumberToContext(prng.getNext(), 5));
const textToStroke = generateRandomWord(prng, wordLength);
context.font = `${area.height / fontSizeFactor}px aafakefontaa`;
context.strokeText(
textToStroke,
adaptRandomNumberToContext(prng.getNext(), area.width),
adaptRandomNumberToContext(prng.getNext(), area.height),
adaptRandomNumberToContext(prng.getNext(), area.width)
)
},
function bezierCurve (prng, context, area) {
context.beginPath();
context.moveTo(
adaptRandomNumberToContext(prng.getNext(), area.width),
adaptRandomNumberToContext(prng.getNext(), area.height)
);
context.bezierCurveTo(
adaptRandomNumberToContext(prng.getNext(), area.width),
adaptRandomNumberToContext(prng.getNext(), area.height),
adaptRandomNumberToContext(prng.getNext(), area.width),
adaptRandomNumberToContext(prng.getNext(), area.height),
adaptRandomNumberToContext(prng.getNext(), area.width),
adaptRandomNumberToContext(prng.getNext(), area.height)
);
context.stroke()
},
function quadraticCurve(prng, context, area) {
context.beginPath();
context.moveTo(
adaptRandomNumberToContext(prng.getNext(), area.width),
adaptRandomNumberToContext(prng.getNext(), area.height)
);
context.quadraticCurveTo(
adaptRandomNumberToContext(prng.getNext(), area.width),
adaptRandomNumberToContext(prng.getNext(), area.height),
adaptRandomNumberToContext(prng.getNext(), area.width),
adaptRandomNumberToContext(prng.getNext(), area.height)
);
context.stroke()
},
function ellipse(prng, context, area) {
context.beginPath();
context.ellipse(
adaptRandomNumberToContext(prng.getNext(), area.width),
adaptRandomNumberToContext(prng.getNext(), area.height),
adaptRandomNumberToContext(prng.getNext(), Math.floor(area.width/2)),
adaptRandomNumberToContext(prng.getNext(), Math.floor(area.height/2)),
adaptRandomNumberToContext(prng.getNext(), 2 * Math.PI, true),
adaptRandomNumberToContext(prng.getNext(), 2 * Math.PI, true),
adaptRandomNumberToContext(prng.getNext(), 2 * Math.PI, true)
);
context.stroke()
}
];
try {
const prng = new Prng(seed);
const canvasElt = document.createElement("canvas");
canvasElt.width = area.width;
canvasElt.height = area.height;
canvasElt.style.display = "none";
const context = canvasElt.getContext("2d");
for (let i = 0; i < roundNumber; i++) {
addRandomCanvasGradient(prng, context, area);
context.shadowBlur = adaptRandomNumberToContext(prng.getNext(), maxShadowBlur);
context.shadowColor = colors[adaptRandomNumberToContext(prng.getNext(), colors.length)];
const randomPrimitive = primitives[adaptRandomNumberToContext(prng.getNext(), primitives.length)];
randomPrimitive(prng, context, area);
context.fill()
}
return x64hash128(canvasElt.toDataURL(), seed);
} catch (e) {}
}
================================================
FILE: test/test.js
================================================
const assert = require('assert');
const puppeteer = require('puppeteer');
const fs = require('fs');
describe('Picasso canvas fingerprinting', function() {
this.timeout(10000);
it('Picasso value should be consistent', async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
const picassoCode = fs.readFileSync('./src/canvas.js', 'utf8');
await page.addScriptTag({content: picassoCode});
const res = await page.evaluate(() => {
const params = {
area: {
width: 300,
height: 300,
},
offsetParameter: 2001000001,
fontSizeFactor: 1.5,
multiplier: 15000,
maxShadowBlur: 50,
};
const numShapes = 5;
const initialSeed = Math.floor(100*Math.random());
const canvasValue = picassoCanvas(
numShapes, initialSeed, params
);
return canvasValue;
})
await browser.close();
assert.equal(typeof res, 'string');
assert.equal(res.length > 15, true);
});
});
gitextract_wymqla_6/
├── .github/
│ └── workflows/
│ └── actions.yml
├── .gitignore
├── LICENSE
├── README.md
├── examples/
│ └── demo.html
├── package.json
├── src/
│ └── canvas.js
└── test/
└── test.js
SYMBOL INDEX (8 symbols across 1 files)
FILE: src/canvas.js
function x64Add (line 1) | function x64Add(m, n) {
function x64Multiply (line 20) | function x64Multiply(m, n) {
function x64Rotl (line 47) | function x64Rotl(m, n) {
function x64LeftShift (line 59) | function x64LeftShift(m, n) {
function x64Xor (line 70) | function x64Xor(m, n) {
function x64Fmix (line 74) | function x64Fmix(h) {
function x64hash128 (line 83) | function x64hash128(key, seed) {
function picassoCanvas (line 165) | function picassoCanvas(roundNumber, seed, params) {
Condensed preview — 8 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (22K chars).
[
{
"path": ".github/workflows/actions.yml",
"chars": 283,
"preview": "name: picasso-canvas-fingerprinting\non: [push]\njobs:\n check-picasso-consistency:\n runs-on: ubuntu-latest\n steps:\n"
},
{
"path": ".gitignore",
"chars": 2047,
"preview": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n.pnpm-debug.log*\n\n# Diagnostic reports"
},
{
"path": "LICENSE",
"chars": 1071,
"preview": "MIT License\n\nCopyright (c) 2019 antoine vastel\n\nPermission is hereby granted, free of charge, to any person obtaining a "
},
{
"path": "README.md",
"chars": 2132,
"preview": "# Picasso based canvas fingerprinting\n\n[ {\n m = [m[0] >>> 16, m[0] & 0xffff, m[1] >>> 16, m[1] & 0xffff];\n n = [n[0] >>> 16, n[0] & 0"
},
{
"path": "test/test.js",
"chars": 1200,
"preview": "const assert = require('assert');\nconst puppeteer = require('puppeteer');\nconst fs = require('fs');\n\ndescribe('Picasso c"
}
]
About this extraction
This page contains the full source code of the antoinevastel/picasso-like-canvas-fingerprinting GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 8 files (20.4 KB), approximately 6.5k tokens, and a symbol index with 8 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.