ltk_webkit/wpe/
favicon.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>

//! The page's icon. WPE WebKit has no favicon API, so the page rasterises it itself.

use std::ffi::{ c_char, CStr };
use std::ptr;
use std::sync::{ Arc, Mutex, Weak };

use gio::ffi::GAsyncResult;
use glib_sys::{ g_error_free, g_free, gboolean, gpointer, GError };
use gobject_sys::{ g_object_unref, GObject };
use ltk::ImageData;
use wpe_webkit_sys::{ webkit_web_view_call_async_javascript_function, WebKitWebView };

use super::scripting::{ webkit_web_view_call_async_javascript_function_finish, JSCValue };
use super::{ Inner, WebView };

const SIZE: u32 = 64;

const WORLD: &[ u8 ] = b"ltk-webkit-favicon\0";

/// Resolves to the icon as `SIZE` x `SIZE` straight RGBA in hex; rejects when there is none.
const SCRIPT: &str = r#"
const link = document.querySelector( 'link[rel~="icon"]' );
const img  = new Image();
img.crossOrigin = 'anonymous';
img.src = link ? link.href : new URL( '/favicon.ico', location.href ).href;
await img.decode();
const canvas = document.createElement( 'canvas' );
canvas.width = canvas.height = 64;
const ctx = canvas.getContext( '2d' );
ctx.drawImage( img, 0, 0, 64, 64 );
let hex = '';
for ( const b of ctx.getImageData( 0, 0, 64, 64 ).data ) hex += ( b < 16 ? '0' : '' ) + b.toString( 16 );
return hex;
"#;

// The generated bindings skip everything that mentions JavaScriptCore.
extern "C"
{
	fn jsc_value_is_string( value: *mut JSCValue ) -> gboolean;
	fn jsc_value_to_string( value: *mut JSCValue ) -> *mut c_char;
}

impl WebView
{
	/// The icon found by the last [`WebView::request_favicon`] that finished.
	pub fn favicon( &self ) -> Option<ImageData>
	{
		self.inner.lock().ok()?.favicon.clone()
	}

	/// Look for the current page's icon. The previous one stays until the answer arrives.
	pub fn request_favicon( &self )
	{
		let webview = match self.inner.lock() { Ok( g ) => g.webview, Err( _ ) => return };
		if webview.is_null() { return; }
		let data = Box::into_raw( Box::new( Arc::downgrade( &self.inner ) ) ) as gpointer;
		// SAFETY: `webview` lives as long as `self`; GIO calls `on_result` exactly once, which reclaims `data`.
		unsafe
		{
			webkit_web_view_call_async_javascript_function(
				webview,
				SCRIPT.as_ptr() as *const c_char,
				SCRIPT.len() as isize,
				ptr::null_mut(),
				WORLD.as_ptr() as *const c_char,
				ptr::null(),
				ptr::null_mut(),
				Some( on_result ),
				data,
			);
		}
	}
}

unsafe extern "C" fn on_result( source: *mut GObject, result: *mut GAsyncResult, user_data: gpointer )
{
	// SAFETY: boxed in `request_favicon`.
	let inner = unsafe { Box::from_raw( user_data as *mut Weak<Mutex<Inner>> ) };
	let mut err: *mut GError = ptr::null_mut();
	// SAFETY: `source` is the view the call was made on, alive for the callback.
	let icon = unsafe
	{
		let value = webkit_web_view_call_async_javascript_function_finish( source as *mut WebKitWebView, result, &mut err );
		if !err.is_null() { g_error_free( err ); }
		take_pixels( value )
	};
	let Some( inner ) = inner.upgrade() else { return };
	if let Ok( mut g ) = inner.lock() { g.favicon = icon; };
}

unsafe fn take_pixels( value: *mut JSCValue ) -> Option<ImageData>
{
	if value.is_null() { return None; }
	// SAFETY: `value` is a full reference from `_finish`; the string is ours to free.
	let hex = unsafe
	{
		let text = if jsc_value_is_string( value ) != 0 { jsc_value_to_string( value ) } else { ptr::null_mut() };
		g_object_unref( value as *mut GObject );
		if text.is_null() { return None; }
		let owned = CStr::from_ptr( text ).to_bytes().to_vec();
		g_free( text as gpointer );
		owned
	};
	let rgba = decode_hex( &hex )?;
	if rgba.len() != ( SIZE * SIZE * 4 ) as usize { return None; }
	Some( ( Arc::new( rgba ), SIZE, SIZE ) )
}

fn decode_hex( hex: &[ u8 ] ) -> Option<Vec<u8>>
{
	if hex.len() % 2 != 0 { return None; }
	hex.chunks_exact( 2 )
		.map( |pair| Some( ( nibble( pair[ 0 ] )? << 4 ) | nibble( pair[ 1 ] )? ) )
		.collect()
}

fn nibble( c: u8 ) -> Option<u8>
{
	( c as char ).to_digit( 16 ).map( |d| d as u8 )
}

#[ cfg( test ) ]
mod tests
{
	use super::*;

	#[ test ]
	fn hex_decodes_and_rejects_garbage()
	{
		assert_eq!( decode_hex( b"00ff7f" ), Some( vec![ 0, 255, 127 ] ) );
		assert_eq!( decode_hex( b"0" ), None );
		assert_eq!( decode_hex( b"zz" ), None );
	}
}