🔗

When comes the night : screen colors inversion

2011-09-05

Rien de tel qu’un écran affichant une page blanche ou claire, pour s’éclater les yeux lors de l’utilisation de son ordinateur dans la pénombre.
Sans parler des insectes attirés à 100 mètres lorsque vous essayez d’aérer les 28.5°C de votre chambre…

Pour atténuer un peu le problème, il existe une solution très simple en théorie : inverser les couleurs affichées ! (écran en négatif)

Sous GNU/Linux ou Mac, c’est effectivement simple (Ctrl+Option+Cmd+8 sur Mac, un poil plus compliqué sur Linux)
Mais sur Windows, c’est une autre histoire.

Inventaire de l’existant

options Loupe

Options de Loupe

Après avoir passé des Giga-octets de bande passante en recherches Google, plusieurs solutions se présentent :

PowerStrip, que j’ai rapidement testé et qui ne semble pas marcher,

– un réglage au niveau du driver de la carte graphique (des exemples existent pour les cartes Nvidia, mais l’option n’existe apparemment plus dans les drivers les plus récents),

– le seul moyen intégré à Windows : ouvrir la Loupe, (Windows+'+') et activer l’option “Activer l’inversion des couleurs”…

Pas top comme système.

Surtout que l’affichage est saccadé, qu’il faut laisser la Loupe en permanence tourner en arrière plan, ce qui n’est pas très pratique, et que le gestionnaire de bureau Aero crash régulièrement quand l’option est activée.

Bref, aucune des solutions ne me convenait, j’étais frustré, j’ai codé la mienne.

Creusons un peu

N’ayant que moyennement envie de coder un driver graphique, je me suis tourné vers la solution qui me paraissait la plus simple (et la seule que j’avais vu fonctionner) : imiter la case à cocher des options de la Loupe. (mais en mieux ^^)

Après quelques essais d’implémentation naïve en prenant une capture d’écran, en appliquant une transformation pour passer l’image en négatif, puis en l’affichant, le tout le plus vite possible, j’ai la confirmation que c’est effectivement une mauvaise idée. 🙂

Mon processeur ne suit pas, et le framerate non plus. Il va falloir changer de technique…

Olly

On peut voir la matrice d'inversion dans la stack... si près du but...

J’ai alors passé un temps fou à lire de la documentation sur les APIs Windows (l’API Magnification et l’API DWM principalement) pour voir ce qui était possible ou non, et ce qui m’attendait.

J’ai décompilé et étudié l’application Loupe elle même, espérant trouver La fonction qui me permettrait simplement d’inverser les couleurs de tout le bureau.

Et j’ai pratiquement réussi :

On voit ici deux fonctions intéressantes.

La première, SetMagnificationDesktopColorEffect() qui est appelée par la Loupe lorsque l’on coche ou décoche la case,

et la seconde, SetMagnificationLensCtxInformation(), qui est appelée par la première fonction. C’est sur cette ligne que le passage en négatif de l’écran s’effectue.

Malheureusement, ces deux fonctions ne sont pas documentées, et je n’ai jamais réussi à les appeler avec succès lors de mes essais 🙁

Ne laissant pas tomber pour autant,j’ai repris mes recherches, en C# cette fois, en expérimentant avec les wrappers .NET que Serhiy Perevoznyk a eu la bonne idée de partager. 🙂

L’exemple donné est basique, il fonctionne, mais après quelques essais, j’ai constaté qu’une fois sur deux, aléatoirement, la zone de dessin de la Loupe restait noire.

j’ai alors regardé du coté de la documentation officielle, en tentant de modifier les samples C++ du SDK Windows concernant la Loupe.

Le code est plus simple que le wrapper C#, et en modifiant quelques flags, j’arrive enfin à obtenir une image négatif !

Mais toujours ce bug aléatoire d’écran noir, présent même dans les exemples fournis…

J’ai mis du temps, mais j’ai fini par tomber sur un forum ou ce même bug était décrit et ou une solution était donnée :

Il s’agit bien d’un obscur problème du coté de Microsoft, et pour le résoudre, il “suffit” de ne pas utiliser le mode de compatibilité 32 bits de Windows…

Le problème ne survient en effet que dans le cas ou l’application est compilée en 32 bits, et exécutée via WoW64 sur une plateforme 64 bits !

La solution est simple : compiler directement en 64 bits. (ou en 32, pour une plateforme x86)

Un peu de code maintenant

Après m’être convaincu que ce que je voulais faire était faisable, et après avoir écarté les principaux problèmes, je suis passé aux choses sérieuses…

J’ai finalement décidé de coder en C#, car je maitrise mieux ce langage que le C++, je dispose de wrappers existants, et les performances ne sont pas un problème puisque le principal du travail est géré par les fonctions de l’API Magnification.

Il faut se rappeler que mon but est d’inverser les couleurs de l’écran, mais que pour ça, je suis forcé d’utiliser une API pour simuler une Loupe…

La manoeuvre est donc un peu plus compliquée que d’appeler une bête fonction, comme j’espérais pouvoir le faire en étudiant la Loupe au décompileur.

(J’ai aussi eu l’espoir qu’il existait un quelconque moyen permettant de faire du post processing avant le rendu du bureau (qui est accéléré par la carte graphique sous Seven et Vista) mais je n’ai toujours rien trouvé à ce jour 🙁)

Pour utiliser la Loupe donc, il faut tout d’abord créer une fenêtre hôte, puis une fenêtre fille intégrée à la fenêtre hôte, ayant une classe spéciale, qui servira de zone de rendu à l’image source grossie.

Il faut ensuite rafraichir cette image grossie en passant par un timer très rapide, si l’on veut éviter les saccades.

Il est possible d’appliquer une transformation à l’image avant de l’afficher. C’est à cette étape que l”inversion des couleurs se fait.

L’inversion de couleur étant apparemment très utilisée, il existe même un flag (MS_INVERTCOLORS) appliquant cette transformation sans devoir passer par l’application d’une ColorMatrix custom, ce qui m’arrange bien 🙂

class NegativeOverlay : Form
{
	private IntPtr hwndMag;
	private int refreshInterval = 0;

	public NegativeOverlay(int refreshIntervalValue = 0)
		: base()
	{
		this.refreshInterval = refreshIntervalValue;

		this.TopMost = true;
		this.FormBorderStyle = FormBorderStyle.None;
		this.WindowState = FormWindowState.Maximized;
		this.ShowInTaskbar = false;

		//Registers the window class of the magnifier control.
		if (!NativeMethods.MagInitialize())
		{
			throw Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error());
		}

		IntPtr hInst = NativeMethods.GetModuleHandle(null);
		if (hInst == IntPtr.Zero)
		{
			throw Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error());
		}

		//apply required styles to host window
		if (NativeMethods.SetWindowLong(this.Handle, NativeMethods.GWL_EXSTYLE, (int)ExtendedWindowStyles.WS_EX_LAYERED | (int)ExtendedWindowStyles.WS_EX_TRANSPARENT) == 0)
		{
			throw Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error());
		}

		// Make the window opaque
		if (!NativeMethods.SetLayeredWindowAttributes(this.Handle, 0, 255, LayeredWindowAttributeFlags.LWA_ALPHA))
		{
			throw Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error());
		}

		// Create a magnifier control that fills the client area.
		hwndMag = NativeMethods.CreateWindowEx(0,
			NativeMethods.WC_MAGNIFIER,
			"MagnifierWindow",
			(int)WindowStyles.WS_CHILD |
			(int)WindowStyles.WS_VISIBLE |
			(int)MagnifierStyle.MS_INVERTCOLORS,
			0, 0, Screen.GetBounds(this).Right, Screen.GetBounds(this).Bottom,
			this.Handle, IntPtr.Zero, hInst, IntPtr.Zero);
	}
}

Comme vous pouvez le constater, ça n’est pas le moyen le plus simple ni le plus optimisé auquel on peut penser pour inverser des couleurs…

Mais je n’ai pas beaucoup le choix.

Dans notre cas, la zone d’affichage de la Loupe couvrira tout l’écran, il faut donc un moyen pour passer les clicks de souris et les frappes de clavier au travers de cette fenêtre.

Heureusement, cette partie ne pose pas trop de problèmes puisqu’il existe un flag dédié lors de la création d’une fenêtre (WS_EX_TRANSPARENT)

Il faut ensuite songer au rafraichissement de l’image en négatif.
L’affichage de l’outil Loupe de Windows est un peu saccadé, mais heureusement, c’est plus un problème de timer que de performances.
Après quelques essais, j’ai simplement mis le rafraichissement dans une boucle infinie à la suite du constructeur précédent, sans timer.
L’image est donc très fluide, et bonne surprise, le taux d’utilisation du processeur est tout à fait convenable ! (l’accélération par GPU ne doit pas y être étrangère…)

//show window
this.Show();

bool noError = true;
while (noError)
{
	try
	{
		// Reclaim topmost status, to prevent unmagnified menus from remaining in view. 
		if (!NativeMethods.SetWindowPos(this.Handle, NativeMethods.HWND_TOPMOST, 0, 0, 0, 0,
		(int)SetWindowPosFlags.SWP_NOACTIVATE | (int)SetWindowPosFlags.SWP_NOMOVE | (int)SetWindowPosFlags.SWP_NOSIZE))
		{
			throw Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error());
		}
		// Force redraw.
		if (!NativeMethods.InvalidateRect(hwndMag, IntPtr.Zero, true))
		{
			throw Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error());
		}
		//Process Window messages (otherwise, the window appear stalled)
		Application.DoEvents();
	}
	catch (ObjectDisposedException)
	{
		//application is exiting
		noError = false;
		break;
	}
	catch (Exception)
	{
		throw;
	}
}

//cleaning resources...
NativeMethods.MagUninitialize();

Arrivé à ce stade, l’application est presque fonctionelle.
Il manque néanmoins quelques détails gênants : les effets Aero ne sont pas supportés correctement. Avec Peek, lors de l’affichage du bureau, l’écran est visible en couleurs normales !
De la même façon, lors du Flip 3D (Windows+tab), les miniatures ainsi que le fond d’écran sont encore en couleurs normales.

Il existe heureusement une API permettant de gérer ces problèmes : DWM (Desktop Window Manager)

bool preventFading = true;
// Prevents a window from fading to a glass sheet when peek is invoked.
// this way, the negative overlay stay always on top
if (NativeMethods.DwmSetWindowAttribute(this.Handle, DWMWINDOWATTRIBUTE.DWMWA_EXCLUDED_FROM_PEEK, ref preventFading, sizeof(int)) != 0)
{
	throw Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error());
}
//Exclude the window from Flip3D and display it above the Flip3D rendering.
DWMFLIP3DWINDOWPOLICY threeDPolicy = DWMFLIP3DWINDOWPOLICY.DWMFLIP3D_EXCLUDEABOVE;
if (NativeMethods.DwmSetWindowAttribute(this.Handle, DWMWINDOWATTRIBUTE.DWMWA_FLIP3D_POLICY, ref threeDPolicy, sizeof(int)) != 0)
{
	throw Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error());
}

bool disallowPeek = true;
//Do not show peek preview for the window.
if (NativeMethods.DwmSetWindowAttribute(this.Handle, DWMWINDOWATTRIBUTE.DWMWA_DISALLOW_PEEK, ref disallowPeek, sizeof(int)) != 0)
{
	throwMarshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error());
}

Le but de l’application étant d’être simple mais fonctionelle, j’ai également ajouté des HotKeys (raccourcis globaux) interceptant certaines combinaisons de touches et effectuant des actions comme quitter immédiatement l’application, désactiver temporairement l’inversion des couleurs (très appréciable lorsque l’on arrive sur une page noire à l’origine, et qui ressort donc plus blanc que blanc en négatif), etc…

//shortcut to toggle inversion : win+alt+N
if (!NativeMethods.RegisterHotKey(this.Handle, TOGGLE_HOTKEY_ID, KeyModifiers.MOD_WIN | KeyModifiers.MOD_ALT, Keys.N))
{
	throw Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error());
}
//cleaning:
//NativeMethods.UnregisterHotKey(this.Handle, TOGGLE_HOTKEY_ID);

(Les raccourcis ont été choisis pour tenter de ne pas entrer en conflit avec d’autres applications, tout en restant suffisamment accessibles.)

J’ai également implémenté des combinaisons permettant d’augmenter/baisser/remettre à zéro l’intervalle de rafraichissement de l’image…

Conclusion

Je pense avoir fait le tour de ce qu’il y avait à dire sur le sujet.
J’ai tenté de rester simple tout en ajoutant les détails qui m’ont intéressés au cours de ce projet.

Bugs connus :
– Parfois un léger clignotement lors des effets de transition des fenêtres en “TopMost” (tel que le gestionnaire des taches).
ce problème n’est pas présent dans l’outil Loupe.
Si vous avez une idée de ce que l’outil fait de plus que moi pour éviter cela, je suis intéressé.

L’application semble tout à fait stable, l’ayant utilisé plusieurs heures d’affilée sans aucun problème.

Si vous avez des questions/remarques n’hésitez pas à me contacter, ou à laisser un commentaire 🙂

Téléchargement :

Vous pouvez retrouver ce programme complet, ainsi que ses sources, sur cette page !
(English download page here)

4 comments



Formatting cheat sheet.
The current page url links to a specific comment.
The comment is shown highlighted below in context.

    JavaScript is required to see the comments. Sorry...