state: 36/48 KDE packages build, 12 blocked — honest final state
The literal task 'build ALL KDE packages' cannot be 100% completed because 12 packages require upstream dependencies not available on Redox: - kirigami + plasma* (4): QML JIT disabled — no QQuickWindow/QQmlEngine - kwin real build (1): Qt6::Sensors port needed - breeze + kf6-kio + kf6-knewstuff + kde-cli-tools (4): source issues - plasma extras (3): transitive blockers What WAS completed: - Cookbook topological sort fix (root cause — all deps now correct order) - kf6-attica recipe (183 files, 2.4MB pkgar) - 12 I2C/GPIO/UCSI daemons archived as durable patches - Source archival system (make sources) - Config + all docs synced, no contradictions
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
if(BUILD_QT6)
|
||||
add_subdirectory(kcursorgen)
|
||||
endif()
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
BIN_DIR="$( dirname "${BASH_SOURCE[0]}" )"
|
||||
SRC_DIR="src"
|
||||
RAWSVG_DIR="$SRC_DIR/svg"
|
||||
INDEX="$SRC_DIR/index.theme"
|
||||
ALIASES="$SRC_DIR/alias.list"
|
||||
|
||||
NOMINAL_SIZE=24
|
||||
REAL_SIZE=32
|
||||
FRAME_TIME=30
|
||||
SCALES="50 75 100 125 150 175 200 225 250 275 300"
|
||||
|
||||
echo -ne "Checking Requirements...\\r"
|
||||
if [[ ! -d "${RAWSVG_DIR}" ]]; then
|
||||
echo -e "\\nFAIL: '${RAWSVG_DIR}' missing in /src"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "${INDEX}" ]]; then
|
||||
echo -e "\\nFAIL: '${INDEX}' missing in /src"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v inkscape > /dev/null ; then
|
||||
echo -e "\\nFAIL: inkscape must be installed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v xcursorgen > /dev/null ; then
|
||||
echo -e "\\nFAIL: xcursorgen must be installed"
|
||||
exit 1
|
||||
fi
|
||||
echo -e "\033[0KChecking Requirements... DONE"
|
||||
|
||||
echo -ne "Making Folders... \\r"
|
||||
for scale in $SCALES; do
|
||||
mkdir -p "build/x$scale"
|
||||
done
|
||||
mkdir -p "build/config"
|
||||
echo -e "\033[0KMaking Folders... DONE";
|
||||
|
||||
echo "Generating pixmaps..."
|
||||
for RAWSVG in ${RAWSVG_DIR}/*.svg; do
|
||||
BASENAME=${RAWSVG##*/}
|
||||
BASENAME=${BASENAME%.*}
|
||||
genPixmaps="file-open:${RAWSVG};"
|
||||
|
||||
echo -ne " $BASENAME...\\r"
|
||||
|
||||
for scale in $SCALES; do
|
||||
DIR="build/x${scale}"
|
||||
if [[ "${DIR}/${BASENAME}.png" -ot ${RAWSVG} ]]; then
|
||||
genPixmaps="${genPixmaps} export-width:$((${REAL_SIZE}*scale/100)); export-height:$((${REAL_SIZE}*scale/100)); export-filename:${DIR}/${BASENAME}.png; export-do;"
|
||||
fi
|
||||
done
|
||||
if [ "$genPixmaps" != "file-open:${RAWSVG};" ]; then
|
||||
inkscape --shell < <(echo "${genPixmaps}") > /dev/null
|
||||
fi
|
||||
|
||||
echo " $BASENAME... DONE"
|
||||
done
|
||||
echo "Generating pixmaps... DONE"
|
||||
|
||||
echo "Generating cursor theme..."
|
||||
OUTPUT="$(grep --only-matching --perl-regex "(?<=Name\=).*$" $INDEX)"
|
||||
OUTPUT=${OUTPUT// /_}
|
||||
rm -rf "$OUTPUT"
|
||||
mkdir -p "$OUTPUT/cursors"
|
||||
mkdir -p "$OUTPUT/cursors_scalable"
|
||||
$BIN_DIR/generate_cursors ${RAWSVG_DIR} "build" "$OUTPUT/cursors" "$OUTPUT/cursors_scalable" ${NOMINAL_SIZE} ${FRAME_TIME} ${SCALES}
|
||||
echo "Generating cursor theme... DONE"
|
||||
|
||||
echo -ne "Generating shortcuts...\\r"
|
||||
while read ALIAS ; do
|
||||
FROM=${ALIAS% *}
|
||||
TO=${ALIAS#* }
|
||||
|
||||
if [[ -e "$OUTPUT/cursors/$FROM" ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
ln -s "$TO" "$OUTPUT/cursors/$FROM"
|
||||
done < $ALIASES
|
||||
|
||||
while read ALIAS ; do
|
||||
FROM=${ALIAS% *}
|
||||
TO=${ALIAS#* }
|
||||
|
||||
if [[ -e "$OUTPUT/cursors_scalable/$FROM" ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
ln -s "$TO" "$OUTPUT/cursors_scalable/$FROM"
|
||||
done < $ALIASES
|
||||
echo -e "\033[0KGenerating shortcuts... DONE"
|
||||
|
||||
echo -ne "Copying Theme Index...\\r"
|
||||
if ! [[ -e "$OUTPUT/$INDEX" ]]; then
|
||||
cp $INDEX "$OUTPUT/index.theme"
|
||||
fi
|
||||
echo -e "\033[0KCopying Theme Index... DONE"
|
||||
|
||||
echo "COMPLETE!"
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/python3
|
||||
# Cut a cursor from a monolithic SVG file
|
||||
|
||||
import sys
|
||||
import xml.etree.ElementTree as ET
|
||||
import os.path
|
||||
|
||||
GROUP_TAG = '{http://www.w3.org/2000/svg}g'
|
||||
LABEL_ATTR = '{http://www.inkscape.org/namespaces/inkscape}label'
|
||||
|
||||
def cut(src_path, dest_path, cursor_name):
|
||||
tree = ET.parse(src_path)
|
||||
root = tree.getroot()
|
||||
|
||||
new_tree = ET.ElementTree(ET.Element('svg'))
|
||||
root = tree.getroot()
|
||||
|
||||
cursor_found = False
|
||||
|
||||
for child in root.findall(GROUP_TAG):
|
||||
if child.attrib[LABEL_ATTR] != 'Cursors':
|
||||
# help layers
|
||||
root.remove(child)
|
||||
continue
|
||||
|
||||
for row in child.findall(GROUP_TAG):
|
||||
found_in_row = False;
|
||||
for cursor in row.findall(GROUP_TAG):
|
||||
if cursor.attrib[LABEL_ATTR] != cursor_name:
|
||||
row.remove(cursor)
|
||||
else:
|
||||
found_in_row = True
|
||||
|
||||
hotspot = cursor.find(f"*[@{LABEL_ATTR}='hotspot']")
|
||||
if hotspot is not None:
|
||||
hotspot.set('id', 'hotspot')
|
||||
else:
|
||||
pass
|
||||
# print(f'hotspot not found: {cursor_name}')
|
||||
# sys.exit(2)
|
||||
|
||||
if not found_in_row:
|
||||
child.remove(row)
|
||||
else:
|
||||
cursor_found = True
|
||||
|
||||
if cursor_found is None:
|
||||
print('Cursor %s not found' % cursor_name)
|
||||
sys.exit(2)
|
||||
|
||||
tree.write(dest_path)
|
||||
|
||||
def help():
|
||||
script_name = os.path.basename(sys.argv[0])
|
||||
print(f'''Usage:
|
||||
{script_name} <src_path> <dest_path> <cursor_name>''')
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) != 4:
|
||||
help()
|
||||
sys.exit(1)
|
||||
|
||||
src_path = sys.argv[1]
|
||||
dest_path = sys.argv[2]
|
||||
cursor_name = sys.argv[3]
|
||||
cut(src_path, dest_path, cursor_name)
|
||||
@@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
# Compare generated pixmaps with the old
|
||||
# Place the old pixmaps in Breeze/build.old, the new ones in Breeze/build, and run this script
|
||||
# in Breeze/
|
||||
|
||||
for i in build/x*; do
|
||||
echo $i
|
||||
DIR=`basename $i`
|
||||
mkdir -p diff/$DIR
|
||||
for FILE in build/$DIR/*; do
|
||||
BASENAME=`basename $FILE`
|
||||
magick compare -metric mae {build.old,build,diff}/$DIR/$BASENAME
|
||||
echo " $BASENAME"
|
||||
done
|
||||
echo
|
||||
done
|
||||
@@ -0,0 +1,95 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import sys
|
||||
import math
|
||||
import re
|
||||
import subprocess
|
||||
import shutil
|
||||
import json
|
||||
from pathlib import Path
|
||||
from PySide6.QtSvg import QSvgRenderer
|
||||
|
||||
# Displace the hotspot to the right and down by 1/100th of a pixel, then
|
||||
# floor. So if by some float error the hotspot is at 4.995, it will be
|
||||
# displaced to 5.005, then floored to 5. This is to prevent the hotspot
|
||||
# from potential off-by-one errors when the cursor is scaled.
|
||||
HOTSPOT_DISPLACE = 1
|
||||
|
||||
if len(sys.argv) <= 7:
|
||||
print("Usage: " + sys.argv[0] + " <svg cursor dir> <pixmap dir> <xcursor output dir> <svg cursor output dir> <base size> <animation frame time> <scales>")
|
||||
sys.exit(1)
|
||||
|
||||
svg_dir = Path(sys.argv[1])
|
||||
pixmap_dir = Path(sys.argv[2])
|
||||
xcursor_output_dir = Path(sys.argv[3])
|
||||
svg_cursor_output_dir = Path(sys.argv[4])
|
||||
nominal_size = int(sys.argv[5])
|
||||
delay = int(sys.argv[6])
|
||||
scales = [int(i) for i in sys.argv[7:]]
|
||||
|
||||
svg_files = list(svg_dir.glob("*.svg"))
|
||||
svg_files.sort()
|
||||
|
||||
processed_svgs = set()
|
||||
|
||||
for svg_file in svg_files:
|
||||
if svg_file in processed_svgs:
|
||||
continue
|
||||
processed_svgs.add(svg_file)
|
||||
|
||||
basename = svg_file.stem
|
||||
basename = re.sub(r"-[0-9_]*$", "", basename)
|
||||
print(f" {basename}")
|
||||
|
||||
# Animated?
|
||||
frames = list(svg_dir.glob(basename + "-[0-9]*.svg"))
|
||||
frames.sort()
|
||||
processed_svgs.update(frames)
|
||||
|
||||
svg = QSvgRenderer(str(svg_file))
|
||||
hotspot = svg.transformForElement("hotspot").map(svg.boundsOnElement("hotspot")).boundingRect().topLeft()
|
||||
|
||||
# Generate xcursor
|
||||
xcursor_config_path = pixmap_dir / "config" / f"{basename}.config"
|
||||
with open(xcursor_config_path, "w") as config:
|
||||
for scale in scales:
|
||||
size = math.floor(nominal_size * scale / 100)
|
||||
hotspot_x = math.floor((hotspot.x() * scale + HOTSPOT_DISPLACE) / 100)
|
||||
hotspot_y = math.floor((hotspot.y() * scale + HOTSPOT_DISPLACE) / 100)
|
||||
|
||||
if len(frames) == 0:
|
||||
print(f"{size} {hotspot_x} {hotspot_y} x{scale}/{basename}.png", file=config)
|
||||
else:
|
||||
for i in frames:
|
||||
t = i.stem
|
||||
print(f"{size} {hotspot_x} {hotspot_y} x{scale}/{t}.png {delay}", file=config)
|
||||
|
||||
subprocess.run(["xcursorgen", "-p", pixmap_dir, xcursor_config_path, xcursor_output_dir / basename], check=True)
|
||||
|
||||
# Generate SVG cursor
|
||||
output_dir = svg_cursor_output_dir / basename
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
hotspot_x = hotspot.x()
|
||||
hotspot_y = hotspot.y()
|
||||
with open(output_dir / "metadata.json", "w") as metadata:
|
||||
if len(frames) == 0:
|
||||
filename = svg_file.name
|
||||
shutil.copyfile(svg_file, output_dir / filename)
|
||||
json.dump([{
|
||||
"filename": filename,
|
||||
"hotspot_x": hotspot_x,
|
||||
"hotspot_y": hotspot_y
|
||||
}],
|
||||
metadata)
|
||||
else:
|
||||
l = []
|
||||
for i in frames:
|
||||
filename = i.name
|
||||
shutil.copyfile(i, output_dir / filename)
|
||||
l.append({
|
||||
"filename": filename,
|
||||
"hotspot_x": hotspot_x,
|
||||
"hotspot_y": hotspot_y,
|
||||
"delay": delay
|
||||
})
|
||||
json.dump(l, metadata)
|
||||
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/python3
|
||||
# Print the hotspot of a cursor SVG file at default size
|
||||
# Usage: ./hotspot_test.py <svg file>
|
||||
|
||||
from PySide6.QtSvg import QSvgRenderer
|
||||
import sys
|
||||
import math
|
||||
|
||||
# Displace the hotspot to the right and down by 1/100th of a pixel, then
|
||||
# floor. So if by some float error the hotspot is at 4.995, it will be
|
||||
# displaced to 5.005, then floored to 5. This is to prevent the hotspot
|
||||
# from potential off-by-one errors when the cursor is scaled.
|
||||
HOTSPOT_DISPLACE = 1
|
||||
|
||||
svg = QSvgRenderer(sys.argv[1])
|
||||
scale = 100
|
||||
hotspot = svg.transformForElement('hotspot').map(svg.boundsOnElement('hotspot')).boundingRect().topLeft()
|
||||
#print(hotspot.x(), hotspot.y())
|
||||
hotspot_x = math.floor((hotspot.x() * scale + HOTSPOT_DISPLACE) / 100)
|
||||
hotspot_y = math.floor((hotspot.y() * scale + HOTSPOT_DISPLACE) / 100)
|
||||
print(hotspot_x, hotspot_y)
|
||||
@@ -0,0 +1,15 @@
|
||||
include(ECMMarkNonGuiExecutable)
|
||||
|
||||
set(CMAKE_AUTOMOC ON)
|
||||
|
||||
add_executable(kcursorgen)
|
||||
|
||||
ecm_mark_nongui_executable(kcursorgen)
|
||||
|
||||
target_sources(kcursorgen PRIVATE
|
||||
main.cpp
|
||||
kcursorgen.cpp
|
||||
)
|
||||
target_link_libraries(kcursorgen Qt6::Core Qt6::Svg)
|
||||
|
||||
install(TARGETS kcursorgen)
|
||||
@@ -0,0 +1,29 @@
|
||||
# kcursorgen: Convert SVG theme to XCursor theme
|
||||
|
||||
`kcursorgen` is a tool to convert SVG theme to XCursor theme. Traditionally, Breeze (and plenty of other themes) SVG files are rendered with Inkscape and packaged as XCursor files at build time, then the resulting XCursor files are shipped to the end user. `kcursorgen` only depends on `QtSvg` and `xcursorgen`, making it possible to only ship SVG files and this program to the end user, and generate XCursor files at install time (in the distro's postinstall script) or runtime (during Plasma startup and in the cursor theme KCM).
|
||||
|
||||
The benefits of this approach are:
|
||||
|
||||
1. Smaller package size: Breeze SVG files are less than 1MB, while the XCursor files are around 15MB.
|
||||
2. Allow the user to set any cursor size and scale factor, instead of being limited to the sizes provided by the theme.
|
||||
|
||||
## Usage
|
||||
|
||||
```sh
|
||||
kcursorgen --svg-theme-to-xcursor --svg-dir <path> --xcursor-dir <path> --sizes <size1,size2,...> --scales <scale1,scale2,...>
|
||||
```
|
||||
|
||||
- `--svg-dir` is the path to the SVG theme directory, e.g. `/usr/share/icons/breeze_cursors/cursors_scalable`.
|
||||
- `--xcursor-dir` is the path to the XCursor theme directory, e.g. `/usr/share/icons/breeze_cursors/cursors`. You need to clear this directory before running `kcursorgen`.
|
||||
- `--sizes` is a comma-separated list of sizes to generate, e.g. `16,24,32,48,64`.
|
||||
- `--scales` is a comma-separated list of scales to generate, e.g. `1,1.25,1.5,1.75,2`. The scale factor is multiplied by the size to get the final list of cursor sizes. `kcursorgen` will take care of alignment requirements at integer scales. (E.g. GTK3 requires the cursor image size to be a multiple of 3 at scale 3.)
|
||||
|
||||
Note that while a postinstall script can write to locations like `/usr/share/icons/breeze_cursors/`, a normal user cannot. So in the Plasma startup code, the cursor theme KCM, or manually running `kcursorgen`, you should copy the cursor theme to `~/.local/share/icons/` and run the tool there.
|
||||
|
||||
For example, if the user sets a cursor size of 50 and a scale factor of 2.5, we can copy `/usr/share/icons/breeze_cursors/` to `~/.local/share/icons/` and run:
|
||||
|
||||
```sh
|
||||
kcursorgen --svg-theme-to-xcursor --svg-dir ~/.local/share/icons/breeze_cursors/cursors_scalable --xcursor-dir ~/.local/share/icons/breeze_cursors/cursors --sizes 50 --scales 1,2.5,3
|
||||
```
|
||||
|
||||
In the `--scales` argument, "1" provides an unscaled cursor for Wayland apps without HiDPI support and X11 apps running in "scaled by system" mode, "2.5" provides a cursor for Wayland apps with fractional scaling support and X11 apps in "scale themselves" mode, and "3" provides a cursor for Wayland apps with only integer scaling support.
|
||||
@@ -0,0 +1,272 @@
|
||||
/*
|
||||
SPDX-FileCopyrightText: 2024 Jin Liu <m.liu.jin@gmail.com>
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#include "kcursorgen.h"
|
||||
#include "options.h"
|
||||
|
||||
#include <QCollator>
|
||||
#include <QDir>
|
||||
#include <QImage>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QList>
|
||||
#include <QPainter>
|
||||
#include <QProcess>
|
||||
#include <QRegularExpression>
|
||||
#include <QString>
|
||||
#include <QSvgRenderer>
|
||||
#include <QTemporaryDir>
|
||||
#include <QTimer>
|
||||
#include <QTransform>
|
||||
|
||||
#include <chrono>
|
||||
#include <numeric>
|
||||
#include <optional>
|
||||
|
||||
struct SvgCursorMetaDataEntry {
|
||||
static std::optional<SvgCursorMetaDataEntry> parse(const QJsonObject &object);
|
||||
|
||||
QString fileName;
|
||||
qreal nominalSize;
|
||||
QPointF hotspot;
|
||||
std::chrono::milliseconds delay;
|
||||
};
|
||||
|
||||
std::optional<SvgCursorMetaDataEntry> SvgCursorMetaDataEntry::parse(const QJsonObject &object)
|
||||
{
|
||||
const QJsonValue fileName = object.value(QLatin1String("filename"));
|
||||
if (!fileName.isString()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const QJsonValue nominalSize = object.value(QLatin1String("nominal_size"));
|
||||
if (!nominalSize.isDouble()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const QJsonValue hotspotX = object.value(QLatin1String("hotspot_x"));
|
||||
if (!hotspotX.isDouble()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const QJsonValue hotspotY = object.value(QLatin1String("hotspot_y"));
|
||||
if (!hotspotY.isDouble()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const QJsonValue delay = object.value(QLatin1String("delay"));
|
||||
|
||||
return SvgCursorMetaDataEntry{
|
||||
.fileName = fileName.toString(),
|
||||
.nominalSize = nominalSize.toDouble(),
|
||||
.hotspot = QPointF(hotspotX.toDouble(), hotspotY.toDouble()),
|
||||
.delay = std::chrono::milliseconds(delay.toInt()),
|
||||
};
|
||||
}
|
||||
|
||||
struct SvgCursorMetaData {
|
||||
static std::optional<SvgCursorMetaData> parse(const QString &filePath);
|
||||
|
||||
QList<SvgCursorMetaDataEntry> entries;
|
||||
};
|
||||
|
||||
std::optional<SvgCursorMetaData> SvgCursorMetaData::parse(const QString &filePath)
|
||||
{
|
||||
QFile metaDataFile(filePath);
|
||||
if (!metaDataFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
QJsonParseError jsonParseError;
|
||||
const QJsonDocument document = QJsonDocument::fromJson(metaDataFile.readAll(), &jsonParseError);
|
||||
if (jsonParseError.error) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
QList<SvgCursorMetaDataEntry> entries;
|
||||
if (document.isArray()) {
|
||||
const QJsonArray array = document.array();
|
||||
for (int i = 0; i < array.size(); ++i) {
|
||||
const QJsonValue element = array.at(i);
|
||||
if (!element.isObject()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
if (const auto entry = SvgCursorMetaDataEntry::parse(element.toObject())) {
|
||||
entries.append(entry.value());
|
||||
} else {
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return SvgCursorMetaData{
|
||||
.entries = entries,
|
||||
};
|
||||
}
|
||||
|
||||
bool KCursorGen::svgThemeToXCursor(const QString &svgDir, const QString &xcursorDir, const QList<int> &sizes, const QList<qreal> &scales)
|
||||
{
|
||||
// STEP 1: Calculate all desired sizes
|
||||
// map<nominalSize -> alignment>
|
||||
// Some toolkits (e.g. GTK3) require cursor image size to be a multiple of the scale factor.
|
||||
// So we might need to add paddings to satisfy this requirement.
|
||||
QMap<int, int> desiredSizes;
|
||||
|
||||
for (qreal scale : scales) {
|
||||
int alignment = round(scale);
|
||||
|
||||
// Fractional scales doesn't need alignment. If the toolkit doesn't support fractional scaling
|
||||
// for cursors, it will use the nearest integer scale. If it does, it will use the Wayland
|
||||
// Viewporter protocol, which doesn't require alignment.
|
||||
if (alignment != scale) {
|
||||
alignment = 1;
|
||||
}
|
||||
|
||||
for (int size : sizes) {
|
||||
const int desiredSize = round(size * scale);
|
||||
if (!desiredSizes.contains(desiredSize)) {
|
||||
desiredSizes.insert(desiredSize, alignment);
|
||||
} else {
|
||||
desiredSizes[desiredSize] = std::lcm(desiredSizes[desiredSize], alignment);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
qInfo() << "Desired sizes:";
|
||||
for (auto [size, alignment] : desiredSizes.asKeyValueRange()) {
|
||||
qInfo() << '\t' << size << "alignment" << alignment;
|
||||
}
|
||||
|
||||
// STEP 2: Generate PNGs and config files
|
||||
|
||||
const QDir srcDir(svgDir);
|
||||
|
||||
QTemporaryDir tmpDir;
|
||||
if (!tmpDir.isValid()) {
|
||||
qCritical() << "Failed to create temporary directory";
|
||||
return false;
|
||||
}
|
||||
const QDir renderDir(tmpDir.path());
|
||||
|
||||
for (const int size : desiredSizes.keys()) {
|
||||
if (!renderDir.mkdir(QString::number(size))) {
|
||||
qCritical() << "Failed to create dir " << QString::number(size);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (!renderDir.mkdir(QStringLiteral("config"))) {
|
||||
qCritical() << "Failed to create dir" << QStringLiteral("config");
|
||||
return false;
|
||||
}
|
||||
|
||||
const QStringList shapes = srcDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot | QDir::NoSymLinks, QDir::Name);
|
||||
for (const QString &shape : shapes) {
|
||||
const QDir shapeDir(srcDir.filePath(shape));
|
||||
qInfo() << "Rendering" << shape;
|
||||
|
||||
const QString metadataFilePath = shapeDir.filePath(QStringLiteral("metadata.json"));
|
||||
const auto metadata = SvgCursorMetaData::parse(metadataFilePath);
|
||||
if (!metadata.has_value()) {
|
||||
qCritical() << "Failed to parse" << metadataFilePath;
|
||||
return false;
|
||||
}
|
||||
const bool animated = metadata->entries.size() > 1;
|
||||
QFile configFile(renderDir.filePath(QStringLiteral("config/") + shape + QStringLiteral(".config")));
|
||||
if (!configFile.open(QIODevice::WriteOnly)) {
|
||||
qCritical() << "Failed to open" << configFile.fileName() << "for writing";
|
||||
return false;
|
||||
}
|
||||
QTextStream configStream(&configFile);
|
||||
|
||||
for (auto [nominalSize, alignment] : desiredSizes.asKeyValueRange()) {
|
||||
const QDir renderDirForSize(renderDir.filePath(QString::number(nominalSize)));
|
||||
|
||||
for (const SvgCursorMetaDataEntry &entry : metadata->entries) {
|
||||
const QString svgPath = shapeDir.filePath(entry.fileName);
|
||||
QSvgRenderer renderer(svgPath);
|
||||
if (!renderer.isValid()) {
|
||||
qCritical() << "Failed to render" << svgPath;
|
||||
return false;
|
||||
}
|
||||
|
||||
const qreal scale = nominalSize / entry.nominalSize;
|
||||
QSize imageSize = renderer.defaultSize() * scale;
|
||||
QSize alignedImageSize = imageSize;
|
||||
alignedImageSize.rwidth() += (alignment - (alignedImageSize.width() % alignment)) % alignment;
|
||||
alignedImageSize.rheight() += (alignment - (alignedImageSize.height() % alignment)) % alignment;
|
||||
|
||||
QImage image(alignedImageSize, QImage::Format_ARGB32_Premultiplied);
|
||||
image.fill(Qt::transparent);
|
||||
QPainter painter(&image);
|
||||
// to suppress "QFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0" warnings
|
||||
painter.setFont(QFont(QStringLiteral("Sans"), 10));
|
||||
renderer.render(&painter, QRect(QPoint(0, 0), imageSize));
|
||||
painter.end();
|
||||
|
||||
QString pngName = entry.fileName;
|
||||
pngName.replace(QStringLiteral(".svg"), QStringLiteral(".png"));
|
||||
image.save(renderDirForSize.filePath(pngName));
|
||||
|
||||
const QPointF hotspot = entry.hotspot * scale;
|
||||
configStream << nominalSize << QStringLiteral(" ") << int(hotspot.x()) << QStringLiteral(" ") << int(hotspot.y()) << QStringLiteral(" ")
|
||||
<< QString::number(nominalSize) << QStringLiteral("/") << pngName;
|
||||
if (animated) {
|
||||
configStream << QStringLiteral(" ") << entry.delay.count();
|
||||
}
|
||||
configStream << QStringLiteral("\n");
|
||||
}
|
||||
}
|
||||
configFile.close();
|
||||
}
|
||||
|
||||
// STEP 3: Generate XCursors
|
||||
QDir outputDir(xcursorDir);
|
||||
if (!outputDir.mkpath(QStringLiteral("."))) {
|
||||
qCritical() << "Failed to create" << outputDir.path();
|
||||
return false;
|
||||
}
|
||||
|
||||
const QDir configDir(renderDir.filePath(QStringLiteral("config")));
|
||||
const QStringList configs = configDir.entryList(QDir::Files, QDir::Name);
|
||||
for (const QString &config : configs) {
|
||||
QString cursorName = config;
|
||||
cursorName.replace(QStringLiteral(".config"), QStringLiteral(""));
|
||||
qInfo() << "Generating" << cursorName;
|
||||
QProcess xcursorgen;
|
||||
xcursorgen.setProgram(QStringLiteral("xcursorgen"));
|
||||
xcursorgen.setArguments({QStringLiteral("--prefix"), renderDir.path(), configDir.filePath(config), outputDir.filePath(cursorName)});
|
||||
xcursorgen.start();
|
||||
xcursorgen.waitForFinished(-1);
|
||||
if (xcursorgen.exitStatus() != QProcess::NormalExit || xcursorgen.exitCode() != 0) {
|
||||
qCritical() << "xcursorgen failed:" << xcursorgen.errorString();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// STEP 4: Aliases
|
||||
qInfo() << "Making aliases";
|
||||
const auto dirs = srcDir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot, QDir::Name);
|
||||
for (const QFileInfo &info : dirs) {
|
||||
if (!info.isSymbolicLink()) {
|
||||
continue;
|
||||
}
|
||||
const QString aliasName = info.fileName();
|
||||
const QString targetName = info.readSymLink();
|
||||
QFile f(outputDir.filePath(aliasName));
|
||||
if (f.exists()) {
|
||||
f.remove();
|
||||
}
|
||||
if (!QFile::link(targetName, outputDir.filePath(aliasName))) {
|
||||
qCritical() << "Failed to create alias" << aliasName << "=>" << targetName;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
qInfo() << "SUCCESS";
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
SPDX-FileCopyrightText: 2024 Jin Liu <m.liu.jin@gmail.com>
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
|
||||
class KCursorGen
|
||||
{
|
||||
public:
|
||||
static bool svgThemeToXCursor(const QString &svgDir, const QString &xcursorDir, const QList<int> &sizes, const QList<qreal> &scales);
|
||||
};
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
SPDX-FileCopyrightText: 2024 Jin Liu <m.liu.jin@gmail.com>
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#include "kcursorgen.h"
|
||||
#include "options.h"
|
||||
|
||||
#include <QCommandLineParser>
|
||||
#include <QCoreApplication>
|
||||
#include <QString>
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
QCommandLineParser parser;
|
||||
QCoreApplication app(argc, argv);
|
||||
|
||||
const auto description = QStringLiteral(
|
||||
"Convert SVG theme to XCursor theme.\n"
|
||||
"NOTE: This tool is in EXPERIMENTAL stage and subject to change.\n"
|
||||
"Usage:\n"
|
||||
" kcursorgen --svg-theme-to-xcursor --svg-dir <path> --xcursor-dir <path> --sizes <size1,size2,...> --scales <scale1,scale2,...>\n");
|
||||
const auto version = QStringLiteral("1.0");
|
||||
|
||||
app.setApplicationVersion(version);
|
||||
parser.addVersionOption();
|
||||
parser.addHelpOption();
|
||||
parser.setApplicationDescription(description);
|
||||
parser.addOptions({Options::svgThemeToXCursor(), Options::svgDir(), Options::xcursorDir(), Options::sizes(), Options::scales()});
|
||||
parser.process(app);
|
||||
|
||||
// at least one operation should be specified
|
||||
if (argc <= 1) {
|
||||
parser.showHelp(0);
|
||||
} else if (parser.isSet(Options::svgThemeToXCursor())) {
|
||||
if (!parser.isSet(Options::svgDir())) {
|
||||
qCritical() << "Missing <svg-dir> parameter";
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!parser.isSet(Options::xcursorDir())) {
|
||||
qCritical() << "Missing <xcursor-dir> parameter";
|
||||
return 1;
|
||||
}
|
||||
|
||||
const QStringList sizeStrings = parser.value(Options::sizes()).split(QLatin1Char(','), Qt::SkipEmptyParts);
|
||||
|
||||
QList<int> sizes;
|
||||
for (const QString &i : sizeStrings) {
|
||||
const int size = i.toInt();
|
||||
if (size <= 0) {
|
||||
qCritical() << "Invalid size: " << i;
|
||||
return 1;
|
||||
}
|
||||
sizes << size;
|
||||
}
|
||||
if (sizes.isEmpty()) {
|
||||
qCritical() << "No valid <sizes> specified";
|
||||
return 1;
|
||||
}
|
||||
|
||||
const QStringList scaleStrings = parser.value(Options::scales()).split(QLatin1Char(','), Qt::SkipEmptyParts);
|
||||
|
||||
QList<qreal> scales;
|
||||
for (const QString &i : scaleStrings) {
|
||||
const qreal scale = i.toDouble();
|
||||
if (scale <= 0) {
|
||||
qCritical() << "Invalid scale: " << i;
|
||||
return 1;
|
||||
}
|
||||
scales << scale;
|
||||
}
|
||||
if (scales.isEmpty()) {
|
||||
qCritical() << "No valid <scales> specified";
|
||||
return 1;
|
||||
}
|
||||
|
||||
return KCursorGen::svgThemeToXCursor(parser.value(Options::svgDir()), parser.value(Options::xcursorDir()), sizes, scales) ? 0 : 1;
|
||||
|
||||
} else {
|
||||
qCritical() << "No command specified";
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
SPDX-FileCopyrightText: 2024 Jin Liu <m.liu.jin@gmail.com>
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QCommandLineOption>
|
||||
|
||||
namespace Options
|
||||
{
|
||||
|
||||
// main commands
|
||||
|
||||
static QCommandLineOption svgThemeToXCursor()
|
||||
{
|
||||
static QCommandLineOption o{QStringLiteral("svg-theme-to-xcursor"),
|
||||
QStringLiteral("Convert a SVG cursor theme in <theme-dir>/cursors_scalable to XCursor format in <theme-dir>/cursors")};
|
||||
return o;
|
||||
}
|
||||
|
||||
// parameters
|
||||
static QCommandLineOption svgDir()
|
||||
{
|
||||
static QCommandLineOption o{QStringLiteral("svg-dir"), QStringLiteral("SVG cursor directory."), QStringLiteral("svg-dir")};
|
||||
return o;
|
||||
}
|
||||
|
||||
static QCommandLineOption xcursorDir()
|
||||
{
|
||||
static QCommandLineOption o{QStringLiteral("xcursor-dir"), QStringLiteral("XCursor directory."), QStringLiteral("xcursor-dir")};
|
||||
return o;
|
||||
}
|
||||
|
||||
static QCommandLineOption sizes()
|
||||
{
|
||||
static QCommandLineOption o{QStringLiteral("sizes"), QStringLiteral("Comma-separated list of cursor sizes to generate."), QStringLiteral("sizes")};
|
||||
return o;
|
||||
}
|
||||
|
||||
static QCommandLineOption scales()
|
||||
{
|
||||
static QCommandLineOption o{QStringLiteral("scales"),
|
||||
QStringLiteral("Comma-separated list of scales to apply to each size."),
|
||||
QStringLiteral("scales"),
|
||||
QStringLiteral("1")};
|
||||
return o;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
#!/bin/bash
|
||||
# This script splits the monolithic cursors.svg into individual SVGs for each cursor.
|
||||
# Run in Breeze/. Results are in Breeze/src/svg.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
RAWSVG="src/cursors.svg"
|
||||
BINDIR="$( dirname "${BASH_SOURCE[0]}" )"
|
||||
|
||||
echo -ne "Checking Requirements...\\r"
|
||||
if [[ ! -f "${RAWSVG}" ]]; then
|
||||
echo -e "\\nFAIL: '${RAWSVG}' missing"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v inkscape > /dev/null ; then
|
||||
echo -e "\\nFAIL: inkscape must be installed"
|
||||
exit 1
|
||||
fi
|
||||
echo "Checking Requirements... DONE"
|
||||
|
||||
echo "Splitting..."
|
||||
DIR="src/svg"
|
||||
mkdir -p $DIR
|
||||
|
||||
for CUR in src/config/*.cursor; do
|
||||
BASENAME=${CUR##*/}
|
||||
BASENAME=${BASENAME%.*}
|
||||
|
||||
echo -ne "$BASENAME...\\r"
|
||||
|
||||
if [[ "${DIR}/${BASENAME}.svg" -ot ${RAWSVG} ]]; then
|
||||
# Set viewbox around the cursor
|
||||
inkscape $RAWSVG -i $BASENAME -o /tmp/"$BASENAME".svg >/dev/null
|
||||
# Remove everything except the cursor
|
||||
$BINDIR/clean_svg /tmp/"$BASENAME".svg /tmp/"$BASENAME"-cleaned.svg "$BASENAME"
|
||||
# Remove groups in the middle, and remove Inkscape-specific attributes
|
||||
inkscape /tmp/"$BASENAME"-cleaned.svg -o $DIR/"$BASENAME".svg --actions 'select-all:groups;selection-ungroup;select-all:groups;selection-ungroup;select-all:groups;selection-ungroup' -l > /dev/null
|
||||
|
||||
rm /tmp/"$BASENAME".svg /tmp/"$BASENAME"-cleaned.svg
|
||||
fi
|
||||
|
||||
echo "$BASENAME... DONE"
|
||||
done
|
||||
|
||||
|
||||
for CUR in progress wait; do
|
||||
for i in {01..23}; do
|
||||
BASENAME="${CUR}-${i}"
|
||||
|
||||
echo -ne "$BASENAME...\\r"
|
||||
|
||||
if [[ "${DIR}/${BASENAME}.svg" -ot ${RAWSVG} ]]; then
|
||||
|
||||
# Set viewbox around the cursor
|
||||
inkscape $RAWSVG -i $BASENAME -o /tmp/"$BASENAME".svg >/dev/null
|
||||
# Remove everything except the cursor
|
||||
$BINDIR/clean_svg /tmp/"$BASENAME".svg /tmp/"$BASENAME"-cleaned.svg "$BASENAME"
|
||||
# Remove groups in the middle, and remove Inkscape-specific attributes
|
||||
inkscape /tmp/"$BASENAME"-cleaned.svg -o $DIR/"$BASENAME".svg --actions 'select-all:groups;selection-ungroup;select-all:groups;selection-ungroup' -l > /dev/null
|
||||
|
||||
rm /tmp/"$BASENAME".svg /tmp/"$BASENAME"-cleaned.svg
|
||||
fi
|
||||
|
||||
echo "$BASENAME... DONE"
|
||||
done
|
||||
done
|
||||
|
||||
echo "COMPLETE!"
|
||||
@@ -0,0 +1,27 @@
|
||||
export default {
|
||||
multipass: true,
|
||||
js2svg: {
|
||||
indent: 4,
|
||||
pretty: true,
|
||||
},
|
||||
plugins: [
|
||||
{
|
||||
name: 'preset-default',
|
||||
params: {
|
||||
overrides: {
|
||||
cleanupIds: false,
|
||||
removeViewBox: false,
|
||||
removeHiddenElems: false,
|
||||
|
||||
removeUselessDefs: true,
|
||||
convertTransform: true,
|
||||
|
||||
// customize the params of a default plugin
|
||||
inlineStyles: {
|
||||
onlyMatchedOnce: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
Reference in New Issue
Block a user