diff --git a/ico/icon-themes.qrc b/ico/icon-themes.qrc index 84c781a0e..ce55438b9 100644 --- a/ico/icon-themes.qrc +++ b/ico/icon-themes.qrc @@ -290,7 +290,6 @@ themes/qet-dark/16x16/conductor-edit.png themes/qet-dark/16x16/configure-toolbars.png themes/qet-dark/16x16/configure.png - themes/qet-dark/16x16/diagram.png themes/qet-dark/16x16/dialog-cancel.png themes/qet-dark/16x16/dialog-ok.png themes/qet-dark/16x16/document-export.png @@ -331,9 +330,6 @@ themes/qet-dark/16x16/folder-open.png themes/qet-dark/16x16/folder-show-all.png themes/qet-dark/16x16/folder.png - themes/qet-dark/16x16/folio-delete.png - themes/qet-dark/16x16/folio-new.png - themes/qet-dark/16x16/folio-properties.png themes/qet-dark/16x16/folio-ref-coming.png themes/qet-dark/16x16/go-company.png themes/qet-dark/16x16/go-down-double.png @@ -348,7 +344,6 @@ themes/qet-dark/16x16/item-copy.png themes/qet-dark/16x16/item-move.png themes/qet-dark/16x16/kdenlive-show-video.png - themes/qet-dark/16x16/label.png themes/qet-dark/16x16/list-add.png themes/qet-dark/16x16/list-remove.png themes/qet-dark/16x16/masquer.png @@ -381,15 +376,7 @@ themes/qet-dark/22x22/arrow-right.png themes/qet-dark/22x22/configure-toolbars.png themes/qet-dark/22x22/configure.png - themes/qet-dark/22x22/diagram.png - themes/qet-dark/22x22/diagram_add.png - themes/qet-dark/22x22/folio-new.png - themes/qet-dark/22x22/diagram_bg.png - themes/qet-dark/22x22/diagram_del.png - themes/qet-dark/22x22/folio-delete.png themes/qet-dark/22x22/dialog-cancel.png - themes/qet-dark/22x22/dialog-information.png - themes/qet-dark/22x22/folio-properties.png themes/qet-dark/22x22/dialog-ok.png themes/qet-dark/22x22/document-export.png themes/qet-dark/22x22/document-import.png @@ -425,7 +412,6 @@ themes/qet-dark/22x22/go-up.png themes/qet-dark/22x22/grid.png themes/qet-dark/22x22/insert-image.png - themes/qet-dark/22x22/label.png themes/qet-dark/22x22/landscape.png themes/qet-dark/22x22/line.png themes/qet-dark/22x22/list-add.png @@ -436,7 +422,6 @@ themes/qet-dark/22x22/object-locked.png themes/qet-dark/22x22/object-rotate-right.png themes/qet-dark/22x22/object-unlocked.png - themes/qet-dark/22x22/pdf-import.png themes/qet-dark/22x22/polygon.png themes/qet-dark/22x22/portrait.png themes/qet-dark/22x22/preferences-desktop-user.png @@ -472,7 +457,6 @@ themes/qet-dark/48x48/user-away.png themes/qet-dark/48x48/view-pim-journal.png themes/qet-dark/128x128/plasmagik.png - themes/qet-dark/128x128/printer.png themes/qet-dark/128x128/settings.png themes/qet-dark/scalable/edit-opacity.svg themes/qet-dark/scalable/image-flip-horizontal.svg diff --git a/ico/themes/qet-dark/128x128/printer.png b/ico/themes/qet-dark/128x128/printer.png deleted file mode 100644 index 6270960f9..000000000 Binary files a/ico/themes/qet-dark/128x128/printer.png and /dev/null differ diff --git a/ico/themes/qet-dark/16x16/diagram.png b/ico/themes/qet-dark/16x16/diagram.png deleted file mode 100644 index 8c9a49b99..000000000 Binary files a/ico/themes/qet-dark/16x16/diagram.png and /dev/null differ diff --git a/ico/themes/qet-dark/16x16/folio-delete.png b/ico/themes/qet-dark/16x16/folio-delete.png deleted file mode 100644 index f1a9f324a..000000000 Binary files a/ico/themes/qet-dark/16x16/folio-delete.png and /dev/null differ diff --git a/ico/themes/qet-dark/16x16/folio-new.png b/ico/themes/qet-dark/16x16/folio-new.png deleted file mode 100644 index 392a4c3ac..000000000 Binary files a/ico/themes/qet-dark/16x16/folio-new.png and /dev/null differ diff --git a/ico/themes/qet-dark/16x16/folio-properties.png b/ico/themes/qet-dark/16x16/folio-properties.png deleted file mode 100644 index b7d5f8091..000000000 Binary files a/ico/themes/qet-dark/16x16/folio-properties.png and /dev/null differ diff --git a/ico/themes/qet-dark/16x16/label.png b/ico/themes/qet-dark/16x16/label.png deleted file mode 100644 index 69668b163..000000000 Binary files a/ico/themes/qet-dark/16x16/label.png and /dev/null differ diff --git a/ico/themes/qet-dark/22x22/diagram.png b/ico/themes/qet-dark/22x22/diagram.png deleted file mode 100644 index a50d55fff..000000000 Binary files a/ico/themes/qet-dark/22x22/diagram.png and /dev/null differ diff --git a/ico/themes/qet-dark/22x22/diagram_add.png b/ico/themes/qet-dark/22x22/diagram_add.png deleted file mode 100644 index b9cd38bdc..000000000 Binary files a/ico/themes/qet-dark/22x22/diagram_add.png and /dev/null differ diff --git a/ico/themes/qet-dark/22x22/diagram_bg.png b/ico/themes/qet-dark/22x22/diagram_bg.png deleted file mode 100644 index 069b8d7eb..000000000 Binary files a/ico/themes/qet-dark/22x22/diagram_bg.png and /dev/null differ diff --git a/ico/themes/qet-dark/22x22/diagram_del.png b/ico/themes/qet-dark/22x22/diagram_del.png deleted file mode 100644 index a4c24e4b7..000000000 Binary files a/ico/themes/qet-dark/22x22/diagram_del.png and /dev/null differ diff --git a/ico/themes/qet-dark/22x22/dialog-information.png b/ico/themes/qet-dark/22x22/dialog-information.png deleted file mode 100644 index ba5792420..000000000 Binary files a/ico/themes/qet-dark/22x22/dialog-information.png and /dev/null differ diff --git a/ico/themes/qet-dark/22x22/folio-delete.png b/ico/themes/qet-dark/22x22/folio-delete.png deleted file mode 100644 index a4c24e4b7..000000000 Binary files a/ico/themes/qet-dark/22x22/folio-delete.png and /dev/null differ diff --git a/ico/themes/qet-dark/22x22/folio-new.png b/ico/themes/qet-dark/22x22/folio-new.png deleted file mode 100644 index b9cd38bdc..000000000 Binary files a/ico/themes/qet-dark/22x22/folio-new.png and /dev/null differ diff --git a/ico/themes/qet-dark/22x22/folio-properties.png b/ico/themes/qet-dark/22x22/folio-properties.png deleted file mode 100644 index ba5792420..000000000 Binary files a/ico/themes/qet-dark/22x22/folio-properties.png and /dev/null differ diff --git a/ico/themes/qet-dark/22x22/label.png b/ico/themes/qet-dark/22x22/label.png deleted file mode 100644 index 32b9b0fea..000000000 Binary files a/ico/themes/qet-dark/22x22/label.png and /dev/null differ diff --git a/ico/themes/qet-dark/22x22/pdf-import.png b/ico/themes/qet-dark/22x22/pdf-import.png deleted file mode 100644 index 6b4bf74f0..000000000 Binary files a/ico/themes/qet-dark/22x22/pdf-import.png and /dev/null differ diff --git a/misc/make_icon_themes.py b/misc/make_icon_themes.py index 48a8fb50f..ec1eca287 100644 --- a/misc/make_icon_themes.py +++ b/misc/make_icon_themes.py @@ -38,6 +38,13 @@ An icon counts as line art when fewer than 20% of its visible pixels are saturated. Its dark copy keeps hue and alpha and inverts lightness, scaled so pure black becomes (220, 220, 220), the dark palette's text color. +An icon drawn as a light object is left alone even when it is not +saturated: a page sheet, a printer body, the white half of the background +swatch. Such art already reads on a dark toolbar, and inverting it would +turn the white body black (the PDF import icon, GitHub #919). The test is +the share of visible pixels that are near white: 30% or more and the icon +inherits from the light theme untouched. + Requires Pillow. Idempotent: running it twice changes nothing. """ @@ -85,6 +92,8 @@ SVGS = [ ] SATURATED_FRACTION = 0.20 # at or above this an icon is "colored" +WHITE_LIGHTNESS = 0.85 # a visible pixel this light counts as white +WHITE_FRACTION = 0.30 # at or above this an icon is "light art" INK = 220 / 255.0 # lightness of pure black after inversion SVG_INK = "#dcdcdc" @@ -107,6 +116,16 @@ def is_line_art(image): return saturated / len(pixels) < SATURATED_FRACTION +def has_light_fill(image): + """True when the icon is mostly white: a page, a sheet, a light body.""" + pixels = visible_pixels(image) + if not pixels: + return False + white = sum(1 for r, g, b, _ in pixels + if colorsys.rgb_to_hls(r / 255, g / 255, b / 255)[1] > WHITE_LIGHTNESS) + return white / len(pixels) >= WHITE_FRACTION + + def invert_lightness(image): """Invert lightness, keeping hue, saturation and alpha. @@ -177,7 +196,7 @@ def main(): light = [] # (alias, source) pairs, paths relative to ico/ dark = [] # paths relative to ico/, files exist on disk changed = 0 - line_art = colored = 0 + line_art = colored = light_art = 0 for size in SIZES: folder = ICO / size @@ -188,7 +207,10 @@ def main(): names.append(ALIASES[rel]) image = Image.open(png).convert("RGBA") art = is_line_art(image) - if art: + if art and has_light_fill(image): + art = False + light_art += 1 + elif art: line_art += 1 dark_image = None else: @@ -213,6 +235,15 @@ def main(): changed += write_if_changed(target, text) dark.append(f"themes/qet-dark/scalable/{name}") + # Drop dark files from an earlier run that are no longer generated, so + # a reclassified icon falls back to the light theme instead of keeping + # a stale copy. + for stale in sorted((THEMES / "qet-dark").glob("*/*")): + if stale.is_file() and stale.name != "index.theme" \ + and str(stale.relative_to(ICO)) not in dark: + stale.unlink() + changed += 1 + dirs = SIZES + ["scalable"] changed += write_if_changed(THEMES / "qet" / "index.theme", index_theme("qet", "QElectroTech icons", dirs)) @@ -231,8 +262,8 @@ def main(): qrc += [" ", "", ""] changed += write_if_changed(QRC, "\n".join(qrc)) - print(f"{line_art} line-art icons, {colored} colored icons, {len(SVGS)} SVGs; " - f"{changed} files written") + print(f"{line_art} line-art icons, {light_art} light icons, {colored} colored icons, " + f"{len(SVGS)} SVGs; {changed} files written or removed") if __name__ == "__main__": diff --git a/tests/qttest/tst_qeticons.cpp b/tests/qttest/tst_qeticons.cpp index db3b95191..1825ffe72 100644 --- a/tests/qttest/tst_qeticons.cpp +++ b/tests/qttest/tst_qeticons.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -34,11 +35,14 @@ table that uses them. Every icon name must resolve in both themes. The dark theme's files - must be light ink. A toolbar button painted with Fusion must show its - icon at 3:1 (WCAG 1.4.11) in both themes, with the disabled state - reading weaker than the enabled one, which is what was reversed on - macOS before (GitHub #466, #870). And the icons the elements panel - draws in its 50 px slots must stay small. + must read on the dark palette, and an icon drawn as a light object + (a page sheet, the PDF import icon of GitHub #919) must come through + the dark theme untouched rather than inverted into a black page. A + toolbar button painted with Fusion must show its icon at 3:1 (WCAG + 1.4.11) in both themes, with the disabled state reading weaker than + the enabled one, which is what was reversed on macOS before (GitHub + #466, #870). And the icons the elements panel draws in its 50 px + slots must stay small. */ class tst_qeticons : public QObject { @@ -47,7 +51,8 @@ class tst_qeticons : public QObject private slots: void initTestCase(); void everyIconResolvesInBothThemes(); - void darkThemeFilesAreLightInk(); + void darkThemeFilesReadOnDarkPalette(); + void lightIconsStayLightInDarkTheme(); void toolbarIconIsReadable_data(); void toolbarIconIsReadable(); void panelProjectIconStaysSmall(); @@ -70,18 +75,26 @@ namespace { return names; } - /// Lightness of the lightest visible pixel: the ink, in a dark-theme file. - double maxLightness(const QImage &image) + /** + Mean color of the visible pixels: what the icon reads as, taken + as a whole, on the background behind it. A page-shaped icon whose + white body was inverted keeps a light border, so the lightest + pixel would pass it; the mean does not. + */ + QColor meanVisibleColor(const QImage &image) { - double max = 0.0; + qint64 r = 0, g = 0, b = 0, n = 0; for (int y = 0; y < image.height(); ++y) for (int x = 0; x < image.width(); ++x) { const QColor c = image.pixelColor(x, y); - if (c.alpha() > 64) max = qMax(max, c.lightnessF()); + if (c.alpha() <= 64) continue; + r += c.red(); g += c.green(); b += c.blue(); ++n; } - return max; + return n ? QColor(r / n, g / n, b / n) : QColor(); } + + const QStringList kSizes = {"16x16", "22x22", "32x32", "48x48", "128x128"}; } void tst_qeticons::initTestCase() @@ -111,27 +124,60 @@ void tst_qeticons::everyIconResolvesInBothThemes() QVERIFY2(!QIcon::fromTheme(name).pixmap(22).isNull(), qPrintable(name)); } -void tst_qeticons::darkThemeFilesAreLightInk() +/** + Every file the generator put in the dark theme, taken as a whole, + must reach 3:1 against the dark palette's window color. Inverted + line art passes by a wide margin; an inverted light object (a white + page turned black) sits below 2:1 and fails, which is the defect + misc/make_icon_themes.py now avoids by leaving such icons to the + light theme. +*/ +void tst_qeticons::darkThemeFilesReadOnDarkPalette() { + const QColor window = QET::Palette::fusionDark().color(QPalette::Active, QPalette::Window); int checked = 0; - for (const QString &size : {"16x16", "22x22", "32x32"}) + for (const QString &size : kSizes) { QDir dir(QString(":/ico/themes/qet-dark/%1").arg(size)); for (const QString &file : dir.entryList({"*.png"}, QDir::Files)) { const QImage dark(dir.filePath(file)); - const QImage light(QString(":/ico/themes/qet/%1/%2").arg(size, file)); - QVERIFY2(!dark.isNull() && !light.isNull(), qPrintable(file)); - // A page-shaped icon keeps its white fill dark after inversion, - // so the mean would mislead; what matters is that the ink is light. - QVERIFY2(maxLightness(dark) >= 0.8, - qPrintable(QString("%1/%2 has no light ink").arg(size, file))); + QVERIFY2(!dark.isNull(), qPrintable(file)); + const QColor mean = meanVisibleColor(dark); + QVERIFY2(mean.isValid(), qPrintable(file)); + const double contrast = QET::Palette::contrastRatio(mean, window); + QVERIFY2(contrast >= kIconRatio, + qPrintable(QString("%1/%2 reads %3:1 on the dark window").arg(size, file).arg(contrast))); ++checked; } } QVERIFY(checked > 100); } +/** + Icons drawn as a light object are not in the dark theme; the theme + inherits them from "qet". Asking the dark theme for them must return + the light art, not a dark page (GitHub #919, the PDF import icon). +*/ +void tst_qeticons::lightIconsStayLightInDarkTheme() +{ + const QColor window = QET::Palette::fusionDark().color(QPalette::Active, QPalette::Window); + QIcon::setThemeName(QStringLiteral("qet-dark")); + for (const QString &name : {"pdf-import", "diagram", "label", "folio-new", "folio-delete", + "folio-properties", "diagram_bg"}) + { + QVERIFY2(!QFile::exists(QString(":/ico/themes/qet-dark/22x22/%1.png").arg(name)), + qPrintable(QString("%1 has a dark copy; the generator inverted a light icon").arg(name))); + const QImage image = QIcon::fromTheme(name).pixmap(22).toImage(); + QVERIFY2(!image.isNull(), qPrintable(name)); + const QColor mean = meanVisibleColor(image); + QVERIFY2(mean.lightnessF() > 0.6, + qPrintable(QString("%1 comes out dark in the dark theme (lightness %2)") + .arg(name).arg(mean.lightnessF()))); + QVERIFY2(QET::Palette::contrastRatio(mean, window) >= kIconRatio, qPrintable(name)); + } +} + void tst_qeticons::toolbarIconIsReadable_data() { QTest::addColumn("theme");