Navigateur portable Linux orienté économie de données et vie privée. Version 1.4 — signal vie privée Sec-GPC, exception d'images par site, robustesse, blocage adverts conforme, build et tests propres.
L'historique complet du projet est dans les tags Git :
v1.2.0(base de référence « éco + robustesse ») etv1.3.0(exception d'images par site).
Save-Data demande un mode économie à qui accepte de l'honorer.
Sec-GPC (Global Privacy Control) est le signal normalisé qui exprime le
consentement de la personne : il est envoyé sur toutes les requêtes, en
plus de Save-Data, et coûte 1 octet.
Les deux sont indépendants (deux interrupteurs distincts, ou un seul pour
Save-Data qui suit l'économie de données). Mesure faite avant d'écrire quoi
que ce soit, sur la machine de développement :
| Échantillon | Sans Sec-GPC |
Avec Sec-GPC |
Écart |
|---|---|---|---|
| 5 images statiques (Google, Bing, YouTube, GitHub) | 134 274 o | 134 274 o | 0 % |
| Vignette Bing extraite d'une recherche d'images | 6 651 o | 6 651 o | 0 % |
Conclusion honnête : aucun gain mesurable sur les ressources joignables depuis cette machine. L'effet documenté de GPC concerne le service de Google et Bing qui choisit la résolution des images ; ces pages n'ont pas livré de vignettes exploitables depuis cette IP (réponse sans image pour Google). Donc :
- on l'active parce que c'est le signal correct et gratuit, pas parce qu'il ferait économiser des octets — aucun chiffre d'économie n'est revendiqué ;
- pour mesurer sur votre propre connexion : onglet Eco → Signal vie privée, puis DevTools → Network → comparer le poids des images avec l'interrupteur activé puis désactivé ;
- désactivable :
secGpc=0danssettings.txt.
Le test test_headers le prouve de bout en bout : un serveur HTTP local reçoit
les requêtes du navigateur et lit les en-têtes — Sec-GPC: 1 est présent
sur la page et sur les images, absent une fois désactivé, et Save-Data reste
indépendant.
| Symptôme | Cause corrigée |
|---|---|
…/ads.js?a=.pdf passait la liste de blocage |
l'extension était cherchée dans l'URL entière : elle est désormais testée sur url.path() seulement |
74 règles ALLOW (-règle) bloquaient au contraire |
le marqueur - n'était pas géré : ces lignes sont ignorées |
24 019 règles ## masquage d'élément bloquaient des URL |
##, #@# sont ignorées (elles ne sont jamais réseau) |
1 059 règles $domain=… appliquées partout |
elles sont ignorées : appliquées globalement, elles bloquaient des sites sans rapport |
Une exception @@ par chemin désactivait tout un domaine |
seules les exceptions « hôte seul » sont prises en compte |
shouldBlock(): 1 900 µs → 9 µs par requête (mesuré, thread IO de QtWebEngine, vraies listes). Index par hôte exact + index des règles libres par jeton, plus de parcours linéaire de toutes les règles à chaque requête.- Plus de
QRegularExpressionreconstruite à chaque requête (156 µs → 1 µs). - La ligne d'état n'est plus écrasée 350 ms après le message : une erreur de chargement ou de téléchargement restait invisible (le timer réécrivait le texte au-dessus). Les messages temporaires ont maintenant la main.
- Compteurs interrogés sur événement et non toutes les 350 ms : c'est ce qui
gonflait
history.db-walà 4 Mo pour 300 lignes.journal_size_limit+wal_checkpoint(TRUNCATE)à la fermeture, écritures en transaction. - La croix de fermeture d'onglet suivait un index figé : après un déplacement d'onglet, elle fermait le mauvais onglet.
m_tabInfosn'était jamais réordonné alors qu'il était indexé par position : la session était sauvegardée dans le désordre.F6ouvrait la page d'erreur de Chromium (setUrl("about:home")au lieu dusetHtmlutilisé partout ailleurs) ; le bouton « Recharger » affichait l'icône « Arrêter » mais ne faisait que recharger.- Crash du renderer : aucun gestionnaire → un onglet blanc définitivement. Une page d'erreur avec « Réessayer » s'affiche maintenant.
- Téléchargements :
deleteLater()sur l'objet émetteur depuis son propre signal, et boîte de dialogue modale imbriquée dans le handler (source du SIGSEGV historique). Résolu parQPointeret exécution différée. - Mode privé : il ne désactivait que les cookies persistants — le profil Qt
continuait d'écrire
localStorage/cache sur le disque. Le profil est maintenant hors disque. - Écran null (QPA headless, machine sans sortie) :
primaryScreen()->…sans test → démarrage en crash. - Session restaurée : liste blanche de schémas (
javascript:,file:,data:sont ignorés) et limite de 12 onglets. - Recherche dans la page, zoom et réglages : une écriture de fichier par cran de molette → écriture groupée. Filtrage de l'historique groupé (250 ms).
- Suggestions réseau : délai maximal (8 s), statut HTTP vérifié, et les résultats sont rattachés à la frappe qui a déclenché la requête (aupour- vant, une frappe rapide affichait les résultats de la requête précédente).
SearXNG/ endpoint de suggestions :javascript:et autres schémas refusés.- Hygiène du dépôt :
.gitignore(profil QtWebEngine, bases SQLite, backups, exports, filtres),settings.txtréel etWebEngineProfile/exclus du dépôt,settings.txt.exemplevierge (le fichier réel contenait une URL de session privée),LICENCEGPL-3.0, attribution des listes de filtres, CI GitHub Actions,.debet métadonnées AppStream.
« Images OFF » est le réglage le plus rentable — mesuré sur une photo 1600×1067 (308 Ko) : 182 Ko en 1280 px, 76 Ko en 800 px, 29 Ko en 480 px, soit −41 % à −91 %. Mais il rend les sites photos, recettes ou documentation illisibles.
Le clic droit propose donc « Images de ce site » (entrée cochable qui reflète l'état réel), et l'autorisation est mémorisée :
- la décision porte sur le site visité, pas sur l'hôte de l'image : les
images viennent presque toujours d'un CDN (
i.ytimg.com,cloudfront…) ; - la correspondance se fait sur les frontières de label :
example.comcouvrewww.example.cometcdn.example.com, mais pasnotexample.com; - le réglage est persisté dans
settings.txt(imageAllow=) ; une entrée contenant un/ou sans point est ignorée à la lecture ; - la page est rechargée après bascule, sinon les images resteraient absentes de l'écran et le réglage semblerait sans effet ;
- la barre d'état indique le nombre d'exceptions :
Eco ON +NoImg/2; - remise à zéro : Menu → Images autorisées : tout réinitialiser (entrée présente seulement s'il existe au moins une exception).
Le curseur « qualité des images » reste, lui, sans effet tant que la compression n'est pas branchée (voir Limite connue).
- Menu contextuel au clic droit : il n'y en avait aucun. Ouverture d'un lien ou d'une image dans un onglet (avant ou de fond), copie du lien / de l'image / de la sélection, recherche de la sélection avec le moteur courant, recharger ou arrêter, zoom, code source, inspecteur, réglages Data saver. Les entrées n'apparaissent que si elles ont du sens (pas de menu à moitié gris), le tout en français — le menu par défaut de Qt est en anglais et QWebEngineView ne le remplace pas de façon fiable.
- Bouton bouclier enfin utile : il était
setEnabled(false)depuis sa création, donc un contrôle mort dans la barre d'outils et dans le parcours de tabulation. Il bascule maintenant le mode économie de données, avec une icône qui reflète l'état. - Infobulles corrigées : elles annonçaient « Alt+clic = nouvel onglet », ce que
le code ne fait pas (
BrowserPage::createWindowrenvoie volontairement l'onglet courant).
Avant : un seul moteur, Mojeek (~5,5 Ko de HTML, mais un index indépendant très réduit → beaucoup de résultats manqués).
Maintenant : 9 moteurs au choix, chacun avec son poids réel mesuré, et une chaîne de repli automatique quand un moteur sert un mur anti-bot.
| Moteur | Poids | JS | Précision |
|---|---|---|---|
| DuckDuckGo (HTML) | ~14 Ko | non | index Bing — défaut |
| SearXNG (méta) | ~10 Ko | non | agrège Google/Bing/DDG — la plus précise |
| Mojeek | ~6 Ko | non | index indépendant, réduit |
| Wiby | ~4 Ko | non | web indie |
| Marginalia | ~40 Ko | non | index niche |
| Startpage | ~22 Ko | oui | résultats Google |
| Yep | ~20 Ko | oui | index communautaire |
| Brave Search | ~300 Ko | oui | très précise, coûteuse |
| ~500 Ko | oui | très coûteuse |
La règle eco tient : le repli automatique ne bascule jamais vers un moteur qui nécessite du JavaScript. Un test le vérifie.
Changer de moteur : Alt+1 … Alt+9, le badge à droite de la barre d'adresse,
ou le panneau Eco.
Deux sources, fusionnées :
- Historique local : 0 octet, instantané.
- API DuckDuckGo : ~200 octets par frappe pour 8 propositions, sans JS ni traçage. Désactivable (panneau Eco → Service de suggestions).
Le debounce est de 170 ms et chaque réponse obsolète est ignorée : taper vite n'attrape pas de rafale de requêtes.
Résolus localement : !w, !so, !gh, !mdn, !yt, !wp, !npm…
Un site direct coûte une requête au lieu d'une page de résultats complète.
!w linux kernel → fr.wikipedia.org (1 requête)
so lambda → stackoverflow.com (1 requête)
gh qt6 → github.com (1 requête)
!sxiu test → moteur (typo gérée : le "!" est retiré)
Certains moteurs répondent 200/202 avec une page de vérification au lieu
des résultats. Sans détection, la recherche paraît marcher mais affiche du
vide. Un script JS de ~700 octets inspecte la page chargée ; si aucun résultat
n'est trouvé, le navigateur bascule sur le moteur suivant et l'indique dans la
barre d'état. Le moteur qui répond est alors adopté : le badge change et les
recherches suivantes l'utilisent, au lieu d'annoncer « DDG » en affichant du
SearXNG.
Quand un moteur sert une page de vérification, le blocage d'images est levé sur cette page. Sans cela, l'utilisateur voit « challenge » mais ne peut pas le résoudre : l'image du captcha est bloquée par le réglage « Images OFF » et le navigateur devient inutilisable. C'est le seul cas où l'économie de données passe au second plan.
Si les moteurs légers sont tous bloqués, le navigateur ne boucle plus : il laisse la vérification affichée (maintenant résolvable) et l'indique.
[←][→][⟳][⌂] [🔒 Rechercher ou saisir une URL ] [🔍DDG ▾] [🛡] [Eco] [☰]
─────────────────────────────────────────────────────────────────────────────────
[🔍 Nouvel onglet ×][🔍 second onglet ×][+] ← barre d'onglets
─────────────────────────────────────────────────────────────────────────────────
Filtres : 114310 · Cache : 0 · Historique : 3 Eco ON · 47 bloqués · ≈2,1Mo Zoom 100 %
Ce qui a changé :
- Barre d'adresse : icône de sécurité (cadenas HTTPS / avertissement HTTP), favicon de l'onglet, badge du moteur cliquable.
- Panneau Eco (
Ctrl+E) : les 4 réglages et le curseur de qualité qui squashaient la barre sont regroupés. Le panneau affiche aussi les compteurs par catégorie (pubs, trackers, polices, médias, images, préchargement, streaming) et l'économie estimée. - Icônes vectorielles dessinées à la volée (
src/ui/icons.cpp) : nettes à toute taille, colorées selon le thème, 0 octet téléchargé, aucun emoji. - Croix de fermeture dessinée (l'icône par défaut de Fusion était un pavé rouge illisible).
- Bouton
+en fin de barre d'onglets, croix qui suit l'onglet déplacé. - Suggestions : liste déroulante avec historique local, sélection au clavier (↑/↓/Tab), repli automatique à la perte de focus ou au changement d'onglet.
- Menu hamburger et raccourcis (
F1) : un seul générateur, testé contre lesQShortcutréels — impossible d'annoncer un raccourci inexistant.
Elles n'étaient pas visibles avant les tests :
| Symptôme | Cause |
|---|---|
| Plantage après 1 frappe dans la barre d'adresse | deleteLater() appelé deux fois sur le même QNetworkReply |
| Page d'accueil illisible | {{ littéral : double échappement au lieu d'un jeton {...} |
localhost:8080 traité comme un schéma |
QUrl lit localhost comme scheme |
% mangés dans le CSS de l'accueil |
.arg() interpreting les % du gradient |
Doublon deleteLater sur la popup |
collision popup ↔ QListWidget |
| Statistiques décalées dans « À propos » | %1 utilisé deux fois dans le même arg() |
Ctrl+D annoncé mais inexistant |
raccourci retiré, liste régénérée depuis le code |
| 10 moteurs pour 9 raccourcis | le 10ᵉ était inaccessible au clavier |
| SIGSEGV à la fermeture | pages détruites après le profil QtWebEngine |
| « Menu → Téléchargements » semble figé | bouton Fermer sans connexion, QMenu encore vivant pendant le dialogue modal, raccourcis dupliqués menu + fenêtre |
| Challenge anti-bot à chaque recherche | images du captcha bloquées par « Images OFF » → vérification impossible à résoudre ; badge moteur resté sur l'ancien moteur alors que le repli en changeait |
sudo apt install qt6-base-dev qt6-webengine-dev libqt6sql6-sqlite cmake g++libqt6sql6-sqlite apporte le moteur SQLite : sans lui, Qt ne sait pas
ouvrir les bases et l'application perd l'historique et le cache silencieusement
(c'est ce que la CI a révélé : aucune base, aucun message). Ce n'est pas un
paquet de développement, il est aussi nécessaire à l'exécution — d'où son
présence dans les dépendances du paquet .deb.
Qt 6.4 minimum (validé par la CI sur Qt 6.4.2 / Ubuntu 24.04 ; testé en
développement sur Qt 6.8). Les rares API plus récentes — délai maximal d'une
requête réseau et politique de permissions persistantes, toutes deux depuis
Qt 6.8 — sont protégées par #if QT_VERSION : le comportement est identique
quelle que soit la version installée.
cd ~/Documents/BrowserECO-Linux
cmake -B build # Release par défaut si rien n'est précisé
cmake --build build -j$(nproc)
./build/BrowserECOOu en une passe, build + tests + installation portable :
./INSTALL.sh # installe dans le dossier du projet
./INSTALL.sh --dest /chemin/portable # ailleurs
./INSTALL.sh --no-tests
./UNINSTALL.sh /chemin/portable # ne supprime que ce qui a été installé./build/BrowserECO --version
./build/BrowserECO --help
./build/BrowserECO --private # profil hors disque, rien n'est écrit
./build/BrowserECO --data-dir ~/Beco # réglages et bases dans un dossier choisi
./build/BrowserECO https://exemple.fr # ouvre l'URL au démarrageUne seule instance par dossier de profil : un second lancement s'arrête avec un
message au lieu de se disputer settings.txt et les bases SQLite avec le
premier.
./run_tests.sh # build + toute la suite
ctest --test-dir build --output-on-failure # tests seuls (déjà construits)
ctest --test-dir build -R test_adblock # une suiteLes tests sont des cibles CMake (tests/CMakeLists.txt) : plus de linkage
manuel des .o. Chaque test a un timeout (120 s pour les tests purs, 300 s
pour ceux qui démarrent QtWebEngine) et s'exécute dans son propre dossier vide,
recréé à chaque exécution — aucun test ne peut voir un settings.txt, une base
ou un profil réel, ni les réglages laissés par un autre test.
18 suites, exécution headless (QT_QPA_PLATFORM=offscreen) :
| Suite | Couverture |
|---|---|
test_adblock |
sémantique ABP (règles -, ##, $domain=, @@, ancres), absence de bypass par query string, performance (µs par requête sur les vraies listes) |
test_searchengine |
résolution URL/recherche, bangs, repli, moteurs JS écartés, refus des URL javascript: |
test_settings |
ancien format (5 lignes) → clé=valeur, bornes, fichiers corrompus |
test_history |
jokers LIKE échappés, suppression, effacement |
test_homepage |
aucun placeholder résiduel, CSS intact |
test_icons |
chaque icône dessine bien des pixels |
test_eco_stats |
cohérence et bornage des compteurs d'économie |
test_images |
exceptions d'images par site : frontières de label, sous-domaines, faux positifs (notexample.com), persistance, idempotence |
test_imagecache |
cache LRU borné en octets (et non en entrées), clé incluant la transformation, compteurs, 4 threads en parallèle |
test_ecoimageurl |
URL signées du serveur local : aller-retour, URL > 400 caractères, et tentatives de forge (signature absente/falsifiée, charge utile remplacée, w/q altérés, mauvais port) |
test_lazy_images |
chargement a la demande : zero requete avant clic, une seule apres, marqueur _eco absent du CDN, compteur exact, page sans image intacte |
test_image_serve |
bout en bout : vrai navigateur + vrai serveur HTTP. Image 1800 px reçue en 1600 px, une seule requête réseau, octets servis < téléchargés, compression off = inchangé, image 404 = page intacte, rechargement bien recompressé |
test_imagecodec |
politique de compression seule (aucun réseau) : refus (seuil, qualité 0, GIF/SVG, données illisibles), redimensionnement (plafond, jamais d'agrandissement, aspect), choix de format, et l'invariant « compressé ⇒ plus léger » vérifié sur 120 combinaisons |
test_headers |
serveur HTTP local qui lit les en-têtes reçus : Sec-GPC et Save-Data partent vraiment, se coupent vraiment, restent indépendants (page et images) |
serp_test |
repli anti-challenge, détection de mur, exemption d'images sur les pages de vérification. 2 cas font de vraies requêtes réseau : ignorés sauf si BROWSERECO_NET_TESTS=1 (un runner CI, en adresse cloud, reçoit un mur anti-bot différent et rendrait le test instable) |
freeze_test |
anti-gel : Menu → Téléchargements, thread témoin |
ui_test |
21 tests d'interface (dont menu contextuel et entrée d'exception d'images) : barre d'adresse, suggestions, panneau Eco, historique, raccourcis, onglets, zoom, raccourcis annoncés = réels, menu contextuel, bouton Eco |
Les données de développement ne sont jamais dans l'arbre source : elles
vont dans ~/.local/share/BrowserECO/dev. On peut donc supprimer build/,
reconfigurer ou faire n'importe quelle manip dans le dépôt sans toucher à un
profil de navigation.
./dev.sh # compile si besoin et lance l'application (données isolées)
./dev.sh test # compile + 12 suites de tests
./dev.sh check # compilation propre (0 warning) + tests + .desktop + version
./dev.sh install [préf.] # installe (par défaut ~/Applications/BrowserECO-dev)
./dev.sh clean # supprime build/
./dev.sh purge-data # supprime les données de dev (demande confirmation)| Copie personnelle | Développement | |
|---|---|---|
| Version | figée sur un tag | en cours |
| Installation | ~/Applications/BrowserECO-<version> |
./dev.sh install |
| Données | ~/.local/share/BrowserECO/perso |
~/.local/share/BrowserECO/dev |
Le dossier de données est choisi par --data-dir (ou BROWSERECO_DATA_DIR) :
on peut donc mettre le binaire sur une clé USB ou une partition NTFS sans
risque, les bases SQLite (WAL) restant sur un disque ext4.
settings.txt, les bases,WebEngineProfile/et les données de navigation ne doivent jamais être committés (.gitignores'en charge) ;- toute correction de bug s'accompagne d'un test dans
tests/; - le format de
settings.txtest centralisé dansSettingsStore, testé partest_settings— ne pas dupliquer la logique de lecture ailleurs ; ./dev.sh checkdoit sortir « TOUT EST VERT » avant un commit ;- version : une seule source,
CMakeLists.txt; les tags Git marquent les versions.
BrowserECO-Linux/
├── CMakeLists.txt ← cible unique, warnings, tests, install, CPack
├── LICENCE ← GPL-3.0 (texte intégral)
├── run_tests.sh ← build + ctest
├── dev.sh ← point d'entrée du développement (run/test/check/install)
├── INSTALL.sh / UNINSTALL.sh ← installation portable (+ .deb via cpack)
├── settings.txt.exemple ← modèle de réglages (sans session)
├── BrowserECO.desktop.in ← modèle d'entrée de menu (généré à l'install)
├── BrowserECO.desktop.example ← idem pour un déploiement portable manuel
├── cmake/ ← version.h.in, install_desktop.cmake.in
├── packaging/ ← AppStream (métadonnées .deb)
├── Filters/update.sh ← récupère EasyList/EasyPrivacy
├── src/
│ ├── main.cpp
│ ├── mainwindow.{h,cpp,ui} ← barre d'adresse, barre d'onglets, menu
│ ├── browserpage.{h,cpp} ← liens internes browseeco://
│ ├── ecointerceptor.{h,cpp} ← blocage + compteurs par catégorie
│ ├── services/
│ │ ├── searchengine.{h,cpp} ← 9 moteurs, bangs, suggestions
│ │ ├── serpguard.{h,cpp} ← détection des murs anti-bot
│ │ ├── settingsstore.{h,cpp} ← lecture/écriture atomique de settings.txt
│ │ ├── adblocker.{h,cpp}
│ │ ├── cachemanager.{h,cpp}
│ │ ├── historymanager.{h,cpp}
│ │ ├── imagecodec.{h,cpp} ← politique de compression (testée seule)
│ │ ├── imagecache.{h,cpp} ← cache LRU des images servies
│ │ ├── ecoimageurl.{h,cpp} ← URL signées du serveur d'images
│ │ └── ecoimageserver.{h,cpp} ← serveur 127.0.0.1 qui sert les images
│ └── ui/
│ ├── omnibox.{h,cpp} ← barre d'adresse + suggestions
│ ├── ecopanel.{h,cpp} ← panneau Data saver
│ └── icons.{h,cpp} ← icônes vectorielles (QPainter)
├── tests/ ← 18 tests + CMakeLists.txt (CTest)
├── Filters/ ← EasyList + EasyPrivacy (~116 000 règles)
│ ├── update.sh ← téléchargement des listes (non versionnées)
│ ├── LICENSE ← attribution obligatoire
│ └── README.md
└── .github/workflows/ci.yml ← build, tests, .desktop, .deb
La version (1.2.0) est écrite une seule fois, dans CMakeLists.txt :
elle est générée dans browsereco_version.h et utilisée par --version, la
fenêtre « À propos » et le User-Agent. Plus de « 1.0.0 » oublié dans main.cpp
pendant que le README annonçait 1.1.
dataSaver=1 # en-tête Save-Data + bloqueurs
imagesOff=1 # pas d'images
ultraEco=0 # + streaming/favicons bloqués
favicons=1 # sauf en mode Ultra
quality=65 # qualité WebP 0-85 (0 = pas de compression)
zoom=1.00
engine=ddg # ddg | searx | mojeek | wiby | marginalia | startpage | yep | brave | google
searxUrl=https://searx.be/search
providerUrl=https://duckduckgo.com/ac/
autoFallback=1 # bascule si mur anti-bot
remoteSuggest=1 # suggestions réseau (~200 o)
restoreSession=0
secGpc=1 # signal Global Privacy Control (Sec-GPC: 1)
imageAllow= # sites dont les images sont autorisées (clic droit)
session= # écrit automatiquement à la sortie (URLs séparées par « | »)Priorité du dossier de profil : --data-dir > variable BROWSERECO_DATA_DIR
dossier du binaire (si inscriptible) >
~/.local/share/BrowserECO.
L'ancien format (5 lignes, qualité seule en 2ᵉ ligne) est lu sans erreur :
le test test_settings couvre ce cas.
| Navigation | |
|---|---|
Ctrl+T / Ctrl+W |
nouvel onglet / fermer |
Ctrl+Tab / Ctrl+Shift+Tab |
onglet suivant / précédent |
Ctrl+L |
barre d'adresse |
Alt+← / Alt+→ |
retour / avancer |
F5 / Ctrl+Shift+R |
recharger / recharger sans cache |
Échap |
arrêter le chargement, fermer les suggestions |
Ctrl+F |
rechercher dans la page |
F6 / F11 |
accueil / plein écran |
Ctrl+molette, Ctrl+0, Ctrl+=, Ctrl+- |
zoom |
| Économie de données | |
|---|---|
Ctrl+E |
panneau Data saver |
Alt+1 … Alt+9 |
changer de moteur |
Ctrl+H / Ctrl+J |
historique / téléchargements |
F1 |
raccourcis |
settings.txt,cache.db,history.dbetWebEngineProfile/à côté du binaire : aucun écrit dans~si le dossier est inscriptible, repli automatique sur~/.local/share/BrowserECOsinon.--private: profil hors disque (aucunWebEngineProfile, aucun localStorage, aucun cache écrit), cache mémoire 20 Mo, historique non écrit.- Dépend des libs Qt6 système, aucun bundling.
# Menu d'applications (installation système : l'entrée est générée avec le
# bon préfixe, y compris Exec= et Icon=)
sudo cmake --install build
update-desktop-database /usr/share/applicationscmake -S . -B build-deb -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX=/usr -DBROWSERECO_DEB_LAYOUT=ON \
-DBROWSERECO_BUILD_TESTS=OFF
cmake --build build-deb -j$(nproc)
cpack --config build-deb/CPackConfig.cmake -G DEB- Code : GPL-3.0 — texte intégral dans
LICENCE. - Listes de filtres : EasyList / EasyPrivacy (tiers, GPLv3 ou CC BY-SA 3.0)
— attribution et conditions dans
Filters/LICENSEetFilters/README.md. Elles ne sont pas incluses dans le dépôt (3,5 Mo) ; sans elles l'application démarre et l'indique (« Filtres : 0 »).
Interrupteur « Images à la demande » dans le panneau Eco : les images sont bloquées au niveau réseau, remplacées visuellement par une bordure pointillée, et un clic ne charge que celle-là.
| Décision | Pourquoi |
|---|---|
| Blocage dans l'intercepteur, pas en JS | Un script DOM arrive trop tard pour les images du HTML initial : le parseur a déjà lancé les requêtes. L'intercepteur est synchrone, donc jamais raté. |
AutoLoadImages n'est PAS coupé |
Mesuré : avec ce réglage off, Chromium refuse tout chargement d'image déclenché par script (restaurer src puis decode() ne charge rien), et un clic ne recharge pas une image dont la src a été remplacée. C'est un impasse. |
Marqueur _eco=1 à la demande |
Le script ne peut pas parler à l'intercepteur autrement qu'en changeant l'URL. L'intercepteur retire le marqueur avant toute requête sortante : le CDN d'origine ne le voit jamais (testé). |
| URLs chargées mémorisées | Sans cela, recharger la page rebloquerait des images que l'utilisateur avait explicitement demandées. Plafonnées à 2048 entrées. |
| Script en monde applicatif | La page ne peut ni voir le script ni le désactiver (un site hostile ne doit pas pouvoir forcer le chargement). |
Ce qui n'est pas couvert (hors périmètre, documenté) : srcset / <picture>,
images en CSS background-image, et tout ce qui n'est pas une balise <img>.
test_lazy_images le prouve dans un vrai navigateur : zéro requête vers le
serveur d'origine tant que l'utilisateur n'a pas cliqué, une seule requête après
clic, marqueur absent du CDN, compteur exact, page sans image intacte, réglage
désactivé = chargement normal.
Le curseur de qualité est branché : chaque image est réécrite vers un petit
serveur HTTP local (127.0.0.1, port éphémère), qui récupère l'original, le
compresse et le sert. Aucune connexion ne sort de la machine.
page ──▶ intercepteur ──redirection──▶ 127.0.0.1:port/i?u=<original>&s=<signature>
│ QNAM + en-têtes rejoués
▼
ImageCodec ──▶ cache LRU ──▶ navigateur
| Décision | Pourquoi |
|---|---|
| Serveur local, pas schéma privé | Qt 6.8 n'honore pas la redirection d'un intercepteur vers un schéma personnalisé pour une sous-ressource : l'original est annulé et rien n'est demandé (mesuré). http://127.0.0.1 est de plus explicitement exempté du blocage « mixed content ». |
| URL signées | Notre URL est une vraie URL http:// : une page pourrait l'appeler. Chaque URL générée est signée avec une clé aléatoire créée au démarrage du processus. Une page peut seulement rejouer une URL qu'elle a déjà vue passer, jamais demander une autre cible (testé : charge utile remplacée, signature falsifiée, w/q altérés, autre port). |
| Jamais de seconde requête | Quand la compression ne sert à rien (GIF, SVG, image sous le seuil), le serveur répond avec les octets déjà reçus. Rediriger vers l'original ferait télécharger l'image deux fois. |
| Échec ouvert | Erreur réseau ou délai dépassé → 502, le navigateur retente l'original et l'image s'affiche. Une image en 404 ne casse pas la page (testé). |
Pas d'Authorization rejouée |
Une redirection du serveur vers un autre hôte la lui transmettrait. |
| CORS absent | Une page ne doit pas pouvoir lire la réponse, même d'une URL signée qu'elle a vue passer. |
Limites assumées, mesurées et documentées :
- Cookies non rejoués :
QWebEngineCookieStoren'expose aucune lecture dans ce Qt. Une image derrière une authentification échoue en403→ l'échec ouvert la fait retenter par le navigateur, donc elle s'affiche, simplement non compressée. - Chrome 142+ : la boucle locale demandera une permission — voir la section dédiée ci-dessous. Sans effet sur le Qt 6.8 embarqué, mais c'est la contrainte qui pèserait le plus lourd si le projet montait en version de Qt.
- Pas de WebP ici : cette installation n'a pas d'encodeur WebP, le codec
bascule en JPEG. Avec
qt6-image-formats-plugins, le gain gagne ~20 % de plus. - Les images en CSS (
background-image) ou construites en JavaScript ne passent pas par l'intercepteur : seules les balises<img>sont réécrites.
Gain mesuré sur de vraies images (fonds d'écran 3840×2160 en PNG) :
| Largeur servie | Octets économisés |
|---|---|
| 1600 px | −79 % à −95 % |
| 1200 px | −87 % à −97 % |
| 800 px | −94 % à −98 % |
Le test test_image_serve rejoue tout cela dans un vrai navigateur contre un
vrai serveur : image reçue en 1600 px au lieu de 1800, une seule requête
réseau pour l'image, et octets servis < octets téléchargés.
C'est la seule partie du projet qui repose sur la documentation et non sur une mesure. Elle mérite donc une explication complète.
Notre serveur d'images écoute sur 127.0.0.1, avec un port éphémère. Google
durecit progressivement l'accès des pages web au réseau local, parce que
« page web → adresse locale » est une vraie surface d'attaque (CSRF vers une box,
un routeur, un appareil connecté).
À partir de Chrome 142, une requête vers la boucle locale demande une permission explicite à l'utilisateur :
| Navigateur | Ce que voit l'utilisateur |
|---|---|
| Chromium de Qt 6.8 (le nôtre) | rien : le trafic passe |
| Chrome 142 et plus | une demande de permission « loopback-network » |
Si l'utilisateur refuse, plus aucune compression d'image. Le navigateur continue de fonctionner normalement : les images se chargent, sans compression.
Trois faits, dont deux mesurés :
- Le schéma privé
ecoimg://ne fonctionne pas (mesuré) : Qt 6.8 annule silencieusement la redirection d'un intercepteur vers un schéma personnalisé pour une sous-ressource. L'original est annulé, rien n'est demandé, et le gestionnaire n'est jamais sollicité. Le serveur local n'est donc pas un choix de confort, c'est la seule voie qui marche sous ce Qt. http://127.0.0.1est explicitement exempté du blocage « mixed content », y compris depuis une page HTTPS en contexte sécurisé (documenté par Chrome).- Le précurseur de cette règle, la PNA, est en mode simple avertissement dans le Chromium embarqué : le trafic passe, seul un avertissement est journalisé dans la console du site.
Le jour où le projet passe à un Qt embarquant Chromium ≥ 142, deux pistes :
- Demander la permission — techniquement simple, mais l'utilisateur voit une demande à chaque visite, ce qui nuit à la crédibilité d'un navigateur « éco ».
- Revenir au schéma privé — c'était le plan initial, et il serait de nouveau possible si le blocage Qt 6.8 était contourné. Attention : ce blocage n'est pas documenté par Qt, il a été déduit experimentally. Il faudrait donc vérifier si un Qt plus récent le corrige, et ne pas supposer que le schéma privé fonctionnerait du premier coup.
Au moment de toute montée de version de Qt :
# Chromium embarqué dans le Qt installé
strings /usr/lib/*/libQt6WebEngineCore.so.* | grep -m1 -oE 'Chrome/[0-9]+'Si le numéro ≥ 142, il faut traiter ce point avant de publier la version.
Le chargement d'images à la demande ne dépend pas du réseau local : il bloque les images au niveau de l'intercepteur et libère au clic. Il n'a aucun rapport avec cette contrainte.