fingerprint_rs/
canvas.rs

1use std::collections::hash_map::DefaultHasher;
2use std::hash::Hasher;
3use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement, Window};
4
5use super::*;
6
7#[derive(Debug, Clone, Copy, PartialEq, Default)]
8pub struct CanvasFingerPrint {
9    pub winding: bool,
10    pub geometry_hash: u64,
11    pub text_hash: u64,
12}
13impl CanvasFingerPrint {
14    pub fn new(window: &Window) -> Option<Self> {
15        let canvas = window
16            .document()?
17            .create_element("canvas")
18            .ok()?
19            .dyn_into::<HtmlCanvasElement>()
20            .ok()?;
21        let ctx = canvas
22            .get_context("2d")
23            .ok()??
24            .dyn_into::<CanvasRenderingContext2d>()
25            .ok()?;
26        let winding = supports_winding(&ctx);
27        render_text_image(&canvas, &ctx);
28        // ignore browsers that add noise to image data.
29        let text = canvas.to_data_url().ok()?;
30        let text_2 = canvas.to_data_url().ok()?;
31        if text != text_2 {
32            return None;
33        }
34        render_geometry_image(&canvas, &ctx);
35        let geometry = canvas.to_data_url().ok()?;
36        let mut hasher = DefaultHasher::new();
37        hasher.write(geometry.as_bytes());
38        let geometry_hash = hasher.finish();
39        let mut hasher = std::collections::hash_map::DefaultHasher::new();
40        hasher.write(text.as_bytes());
41        let text_hash = hasher.finish();
42        Some(Self {
43            winding,
44            geometry_hash,
45            text_hash,
46        })
47    }
48}
49fn render_text_image(canvas: &HtmlCanvasElement, ctx: &CanvasRenderingContext2d) {
50    // Resizing the canvas cleans it
51    canvas.set_width(240);
52    canvas.set_height(60);
53
54    ctx.set_text_baseline("alphabetic");
55    ctx.set_fill_style(&JsValue::from_str("#60"));
56    ctx.fill_rect(100., 1., 62., 20.);
57    ctx.set_fill_style(&JsValue::from_str("#069"));
58    // It's important to use explicit built-in fonts in order to exclude the affect of font preferences
59    // (there is a separate entropy source for them).
60    ctx.set_font(r#"11pt "Times New Roman""#);
61    let printed_text = "Cwm fjordbank gly \u{1F60D}";
62    ctx.fill_text(printed_text, 2., 15.).unwrap();
63    ctx.set_fill_style(&JsValue::from_str("rgba(102, 204, 0, 0.2)"));
64    ctx.set_font("18pt Arial");
65    ctx.fill_text(&printed_text, 4., 45.).unwrap();
66}
67
68fn supports_winding(ctx: &CanvasRenderingContext2d) -> bool {
69    ctx.rect(0., 0., 10., 10.);
70    ctx.rect(2., 2., 6., 6.);
71    ctx.is_point_in_path_with_f64_and_canvas_winding_rule(
72        5.,
73        5.,
74        web_sys::CanvasWindingRule::Evenodd,
75    )
76}
77
78fn render_geometry_image(canvas: &HtmlCanvasElement, ctx: &CanvasRenderingContext2d) {
79    // clear canvas by resizing
80    canvas.set_width(122);
81    canvas.set_height(110);
82
83    // Set global composite operation to 'multiply'
84    ctx.set_global_composite_operation("multiply").unwrap();
85
86    // Draw the circles with different colors
87    let colors_and_positions = [
88        ("#f2f", 40.0, 40.0),
89        ("#2ff", 80.0, 40.0),
90        ("#ff2", 60.0, 80.0),
91    ];
92
93    // draw three different color circles at different positions so their parts of each overlap at the center of the image.
94    for (color, x, y) in colors_and_positions.iter() {
95        ctx.set_fill_style(&JsValue::from_str(color));
96        ctx.begin_path();
97        ctx.arc(*x, *y, 40.0, 0.0, std::f64::consts::PI * 2.0)
98            .unwrap();
99        ctx.close_path();
100        ctx.fill();
101    }
102
103    // Draw the winding rule example
104    ctx.set_fill_style(&JsValue::from_str("#f9c"));
105    ctx.begin_path();
106    ctx.arc(60.0, 60.0, 60.0, 0.0, std::f64::consts::PI * 2.0)
107        .unwrap();
108    ctx.arc(60.0, 60.0, 20.0, 0.0, std::f64::consts::PI * 2.0)
109        .unwrap();
110    ctx.fill_with_canvas_winding_rule(web_sys::CanvasWindingRule::Evenodd);
111}