././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1744983866.5368226
ssfconv-1.2.2/ 0000775 0001750 0001750 00000000000 15000453473 012274 5 ustar 00jimmy jimmy ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1742720619.0
ssfconv-1.2.2/LICENSE 0000664 0001750 0001750 00000001373 14767747153 013331 0 ustar 00jimmy jimmy This package is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
(at your option) any later version.
.
This package is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
.
You should have received a copy of the GNU General Public License
along with this program. If not, see
.
On Debian systems, the complete text of the GNU General
Public License version 3 can be found in "/usr/share/common-licenses/GPL-3".
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1744983866.5368226
ssfconv-1.2.2/PKG-INFO 0000644 0001750 0001750 00000011604 15000453473 013371 0 ustar 00jimmy jimmy Metadata-Version: 2.4
Name: ssfconv
Version: 1.2.2
Summary: Sogou input method skin file (.ssf file) converter
Author: nihui, VOID001, fkxxyz, RadND
License: GPL-3.0-or-later
Project-URL: Repository, https://codeberg.org/radnd/ssfconv.git
Project-URL: Issues, https://codeberg.org/radnd/ssfconv/issues
Keywords: ssf,fcitx,fcitx5,Fcitx5 Classic User interface
Classifier: Programming Language :: Python :: 3
Classifier: Operating System :: OS Independent
Classifier: Environment :: Console
Classifier: Intended Audience :: End Users/Desktop
Classifier: Natural Language :: Chinese (Simplified)
Classifier: Topic :: Adaptive Technologies
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: pycryptodomex
Requires-Dist: pillow
Requires-Dist: numpy
Dynamic: license-file
# 简介

一个将皮肤从搜狗格式转换为 fcitx/fcitx5 格式的脚本,如果您在寻找“原生”的fcitx5皮肤,可以看[这里](https://github.com/topics/fcitx5-theme)
新开了仓库,因为作者的账号很久没有活动过,我所做的改动也很大:
1. 制成 python 包,用户不需要自己装依赖了
2. 重构,减少重复,将原先的脚本拆分为解包和转换两部分
3. 注册命令行程序、调整命令行参数、中文本地化
4. 由于使用了match case语法,python版本要求上升到3.10,如果想用旧版本python运行,请自行把相关代码改回长串 if elif
程序的目的没有变,所以原作者的[参考图像](https://www.fkxxyz.com/d/ssfconv)依然适用
# 安装
## 手动
clone或下载release
```shell
python -m venv venv_name
source venv_name/bin/activate
# source venv_name/bin/activate.fish
pip install .
```
## pip
[ ] 下次一定上传到 pypi
```shell
pip install ssfconv
```
# 使用
本包提供的命令行程序加 `-h` 参数就可以查看帮助
## 获取皮肤
可以从[搜狗的皮肤站](https://pinyin.sogou.com/skins/)或bilibili搜索输入法皮肤获取自己喜欢的皮肤,得到ssf格式的文件,例如 【雨欣】蒲公英的思念.ssf
## 解包 ssf 皮肤
```shell
ssfconv unpack 【雨欣】蒲公英的思念.ssf
```
得到的文件夹可供 `ssfconv` 使用
## 转换为 fcitx4 皮肤
```shell
ssfconv convert -t fcitx 【雨欣】蒲公英的思念
```
复制到用户皮肤目录
```shell
mkdir -p ~/.config/fcitx/skin/
cp -r 【雨欣】蒲公英的思念 ~/.config/fcitx/skin/
```
右键输入法托盘图表,选中皮肤,这款皮肤是不是出现在列表里了呢,尽情享用吧。
## 转换为 fcitx5 主题
```shell
ssfconv convert 【雨欣】蒲公英的思念
```
复制到用户主题目录
```shell
mkdir -p ~/.local/share/fcitx5/themes/
cp -r 【雨欣】蒲公英的思念 ~/.local/share/fcitx5/themes/
```
打开 fcitx5 的配置,附加组件标签,经典用户界面,点配置,在主题的下拉列表里,选择这款皮肤。
或者你也可以直接修改配置文件 ~/.config/fcitx5/conf/classicui.conf,将 Theme 的值改成这个皮肤的名称即可。
查看该皮肤在配置文件中的名称:
```shell
grep Name ~/.local/share/fcitx5/themes/【雨欣】蒲公英的思念/theme.conf
```
# 微调
转换得到的皮肤配置或多或少会有点瑕疵,其实调整它们并不困难,快去试试吧
# 致谢
前两位作者的仓库分别在
- https://github.com/fkxxyz/ssfconv
- https://github.com/VOID001/ssf2fcitx
前两位作者的账号都不活动,也许已经对系统美化不感兴趣了。不管怎样,我把他们写在了 `pyproject.toml` 中,在此对他们表示感谢。( •ᴗ• )
还要感谢 [`那些时光.ssf`](https://pinyin.sogou.com/d/skins/download.php?skin_id=344544) 的作者 szwjerry ,为了用上这个皮肤,我才接触到这个工具。
# 贡献征集
## 方格示意
需要一个调试工具,能依据转换后的皮肤配置文件在图片上划线,用以表明这个皮肤是怎样被拉伸的
考虑到这和转换的过程无关,更像是一个独立的程序
## 更多整理
转换部分的主函数仍然非常长,需要拆分,有部分固定的配置可以抽离出来以模板文件的形式存在
## IBus
```
GNOME桌面和非GNOME桌面的IBus采用了两个不同的前端。非GNOME桌面的IBus是项目自己本身基于GTK写的一个很简陋的前端,其使用GTK主题,根据我之前的研究,要实现搜狗那样的皮肤效果应该不太可能。GNOME在他们的GJS代码库里给IBus重写了一个前端,他们的前端可定制性更强,主要是使用CSS文件指定样式
@HollowMan6
```
仅靠转换程序做不到在 IBus 上实现搜狗这种美观的皮肤效果,看样子最有希望的办法是通过 GNOME Shell 扩展修改输入法前端,先行提供背景图片等素材的显示和拉伸/压缩的能力
readme有点长,其他不重要的内容看 [Wiki 页面](https://github.com/RadND/ssfconv/wiki)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1742721317.0
ssfconv-1.2.2/README.md 0000664 0001750 0001750 00000010123 14767750445 013572 0 ustar 00jimmy jimmy # 简介

一个将皮肤从搜狗格式转换为 fcitx/fcitx5 格式的脚本,如果您在寻找“原生”的fcitx5皮肤,可以看[这里](https://github.com/topics/fcitx5-theme)
新开了仓库,因为作者的账号很久没有活动过,我所做的改动也很大:
1. 制成 python 包,用户不需要自己装依赖了
2. 重构,减少重复,将原先的脚本拆分为解包和转换两部分
3. 注册命令行程序、调整命令行参数、中文本地化
4. 由于使用了match case语法,python版本要求上升到3.10,如果想用旧版本python运行,请自行把相关代码改回长串 if elif
程序的目的没有变,所以原作者的[参考图像](https://www.fkxxyz.com/d/ssfconv)依然适用
# 安装
## 手动
clone或下载release
```shell
python -m venv venv_name
source venv_name/bin/activate
# source venv_name/bin/activate.fish
pip install .
```
## pip
[ ] 下次一定上传到 pypi
```shell
pip install ssfconv
```
# 使用
本包提供的命令行程序加 `-h` 参数就可以查看帮助
## 获取皮肤
可以从[搜狗的皮肤站](https://pinyin.sogou.com/skins/)或bilibili搜索输入法皮肤获取自己喜欢的皮肤,得到ssf格式的文件,例如 【雨欣】蒲公英的思念.ssf
## 解包 ssf 皮肤
```shell
ssfconv unpack 【雨欣】蒲公英的思念.ssf
```
得到的文件夹可供 `ssfconv` 使用
## 转换为 fcitx4 皮肤
```shell
ssfconv convert -t fcitx 【雨欣】蒲公英的思念
```
复制到用户皮肤目录
```shell
mkdir -p ~/.config/fcitx/skin/
cp -r 【雨欣】蒲公英的思念 ~/.config/fcitx/skin/
```
右键输入法托盘图表,选中皮肤,这款皮肤是不是出现在列表里了呢,尽情享用吧。
## 转换为 fcitx5 主题
```shell
ssfconv convert 【雨欣】蒲公英的思念
```
复制到用户主题目录
```shell
mkdir -p ~/.local/share/fcitx5/themes/
cp -r 【雨欣】蒲公英的思念 ~/.local/share/fcitx5/themes/
```
打开 fcitx5 的配置,附加组件标签,经典用户界面,点配置,在主题的下拉列表里,选择这款皮肤。
或者你也可以直接修改配置文件 ~/.config/fcitx5/conf/classicui.conf,将 Theme 的值改成这个皮肤的名称即可。
查看该皮肤在配置文件中的名称:
```shell
grep Name ~/.local/share/fcitx5/themes/【雨欣】蒲公英的思念/theme.conf
```
# 微调
转换得到的皮肤配置或多或少会有点瑕疵,其实调整它们并不困难,快去试试吧
# 致谢
前两位作者的仓库分别在
- https://github.com/fkxxyz/ssfconv
- https://github.com/VOID001/ssf2fcitx
前两位作者的账号都不活动,也许已经对系统美化不感兴趣了。不管怎样,我把他们写在了 `pyproject.toml` 中,在此对他们表示感谢。( •ᴗ• )
还要感谢 [`那些时光.ssf`](https://pinyin.sogou.com/d/skins/download.php?skin_id=344544) 的作者 szwjerry ,为了用上这个皮肤,我才接触到这个工具。
# 贡献征集
## 方格示意
需要一个调试工具,能依据转换后的皮肤配置文件在图片上划线,用以表明这个皮肤是怎样被拉伸的
考虑到这和转换的过程无关,更像是一个独立的程序
## 更多整理
转换部分的主函数仍然非常长,需要拆分,有部分固定的配置可以抽离出来以模板文件的形式存在
## IBus
```
GNOME桌面和非GNOME桌面的IBus采用了两个不同的前端。非GNOME桌面的IBus是项目自己本身基于GTK写的一个很简陋的前端,其使用GTK主题,根据我之前的研究,要实现搜狗那样的皮肤效果应该不太可能。GNOME在他们的GJS代码库里给IBus重写了一个前端,他们的前端可定制性更强,主要是使用CSS文件指定样式
@HollowMan6
```
仅靠转换程序做不到在 IBus 上实现搜狗这种美观的皮肤效果,看样子最有希望的办法是通过 GNOME Shell 扩展修改输入法前端,先行提供背景图片等素材的显示和拉伸/压缩的能力
readme有点长,其他不重要的内容看 [Wiki 页面](https://github.com/RadND/ssfconv/wiki)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1744982564.0
ssfconv-1.2.2/pyproject.toml 0000664 0001750 0001750 00000001677 15000451044 015213 0 ustar 00jimmy jimmy [build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
[project]
name = "ssfconv"
version = "1.2.2"
dependencies = ["pycryptodomex", "pillow", "numpy"]
requires-python = ">=3.10"
authors = [
{ name = "nihui" },
{ name = "VOID001" },
{ name = "fkxxyz" },
{ name = "RadND" },
]
description = "Sogou input method skin file (.ssf file) converter"
readme = "README.md"
keywords = ["ssf", "fcitx", "fcitx5", "Fcitx5 Classic User interface"]
license = { text ="GPL-3.0-or-later" }
classifiers = [
"Programming Language :: Python :: 3",
"Operating System :: OS Independent",
"Environment :: Console",
"Intended Audience :: End Users/Desktop",
"Natural Language :: Chinese (Simplified)",
"Topic :: Adaptive Technologies",
]
[project.urls]
Repository = "https://codeberg.org/radnd/ssfconv.git"
Issues = "https://codeberg.org/radnd/ssfconv/issues"
[project.scripts]
ssfconv = "ssfconv.cli:main"
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1744983866.5368226
ssfconv-1.2.2/setup.cfg 0000664 0001750 0001750 00000000046 15000453473 014115 0 ustar 00jimmy jimmy [egg_info]
tag_build =
tag_date = 0
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1744983866.5328224
ssfconv-1.2.2/src/ 0000775 0001750 0001750 00000000000 15000453473 013063 5 ustar 00jimmy jimmy ././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1744983866.5338223
ssfconv-1.2.2/src/ssfconv/ 0000775 0001750 0001750 00000000000 15000453473 014544 5 ustar 00jimmy jimmy ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1742720619.0
ssfconv-1.2.2/src/ssfconv/__init__.py 0000664 0001750 0001750 00000000002 14767747153 016671 0 ustar 00jimmy jimmy
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1744981473.0
ssfconv-1.2.2/src/ssfconv/cli.py 0000775 0001750 0001750 00000006454 15000446741 015701 0 ustar 00jimmy jimmy import sys, shutil
from pathlib import Path
from .translation import _
import argparse
from .extract.ssf import extract_ssf
from .convert import convert
def add_parser_unpack(subparsers: argparse._SubParsersAction):
parser = subparsers.add_parser("unpack", help=_("Unpack skin"))
parser.add_argument("src", type=Path, help=_("Skin file"))
parser.add_argument(
"dest",
type=Path,
help=_("Output folder name, default is same as Skin file name"),
nargs="?",
default=None,
)
parser.add_argument(
"-t",
"--type",
help=_(
"Format of skin to be unpack",
),
default="ssf",
choices=["ssf"],
)
parser.add_argument(
"-f",
"--force",
help=_("Delete if output path already exist"),
action="store_true",
)
parser.set_defaults(func=unpack)
def unpack(args):
if args.dest == None:
args.dest = args.src.with_suffix("")
if not args.src.exists():
sys.stderr.write(_("Input path %s not exist\n") % args.src)
return 1
if not args.src.is_file():
sys.stderr.write(_("Input path %s is not file\n") % args.src)
return 1
if args.dest.exists():
if args.force:
if args.dest.is_dir():
shutil.rmtree(args.dest)
else:
args.dest.unlink()
else:
sys.stderr.write(_("Output path %s already exist\n") % args.dest)
return 1
else:
args.dest.mkdir()
return extract_ssf(args.src, args.dest)
def add_parser_convert(subparsers: argparse._SubParsersAction):
parser = subparsers.add_parser("convert", help=_("Convert skin file"))
parser.add_argument("src", type=Path, help=_("Input folder"))
# parser.add_argument(
# "dest", help="输出文件夹,默认与输入文件夹相同", nargs="?", default=None
# )
parser.add_argument(
"-t",
"--type",
help=_("Output skin type"),
default="fcitx5",
choices=["fcitx", "fcitx5"],
)
# parser.add_argument(
# "-f", "--force", help="强制覆盖输出文件夹的内容", action="store_true"
# )
parser.add_argument(
"-i",
"--install",
help=_("Install the convert result to it's default install location"),
action="store_true",
)
parser.set_defaults(func=conv)
def conv(args):
if not args.src.exists():
sys.stderr.write(_("Input path %s not exist\n") % args.src)
return 1
if not args.src.is_dir():
sys.stderr.write(_("Input path %s is not folder\n") % args.src)
return 1
args.dest = args.src
if args.dest.exists():
# if not args.force:
# sys.stderr.write("输出文件(夹) %s 已存在\n" % args.dest)
# return 1
pass
else:
args.dest.mkdir()
err = convert(args)
exit(err)
def create_parser():
parser = argparse.ArgumentParser(prog="ssfconv")
# parser.add_argument("--foo", action="store_true", help="foo help")
subparsers = parser.add_subparsers()
add_parser_unpack(subparsers)
add_parser_convert(subparsers)
return parser
def main():
parser = create_parser()
# print(parser.parse_args(["a", "12"]))
args = parser.parse_args()
args.func(args)
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1744983866.5358224
ssfconv-1.2.2/src/ssfconv/convert/ 0000775 0001750 0001750 00000000000 15000453473 016224 5 ustar 00jimmy jimmy ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1742720619.0
ssfconv-1.2.2/src/ssfconv/convert/CaseSensitiveConfigParser.py 0000664 0001750 0001750 00000000303 14767747153 023666 0 ustar 00jimmy jimmy import configparser
# 为了使其区分大小写,重载 ConfigParser
class CaseSensitiveConfigParser(configparser.ConfigParser):
def optionxform(self, optionstr):
return optionstr ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1743303800.0
ssfconv-1.2.2/src/ssfconv/convert/__init__.py 0000664 0001750 0001750 00000001653 14772132170 020345 0 ustar 00jimmy jimmy from .out import ssf2fcitx, ssf2fcitx5
from pathlib import Path
import os, sys, shutil
import logging
def convert(args):
install_dir = None
# TODO refractor ssf2fcitx{5} get rid of os.path
match args.type:
case "fcitx5":
err = ssf2fcitx5(args.src)
case "fcitx":
err = ssf2fcitx(args.src)
case _:
assert False
if args.install:
install(args)
return err
default_skins_dir = {
"fcitx5": "fcitx5/themes/",
"fcitx": "fcitx/skin/",
}
def install(args):
match args.type:
case "fcitx5":
install_dir = os.getenv("XDG_DATA_HOME", Path("~/.local/share").expanduser())
case "fcitx":
install_dir = os.getenv("XDG_CONFIG_HOME", Path("~/.config").expanduser())
case _:
assert False
install_dir = Path(install_dir) / default_skins_dir[args.type]
shutil.move(args.dest, install_dir)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1744980605.0
ssfconv-1.2.2/src/ssfconv/convert/image_operation.py 0000664 0001750 0001750 00000006266 15000445175 021752 0 ustar 00jimmy jimmy from PIL import Image, ImageDraw
import numpy as np
import logging
from pathlib import Path
def getImageAvg(image_path:Path, area=(0, 0, 0, 0)):
"""
获取图片的像素平均值
image_path 图片的路径
aria 是需要求的平均值的区域,默认整幅图
格式 area = (x1,x2,y1,y2)
当 x2 或 y2 为零表示最大值直到边界
为负时表示距离最大边界多少的坐标
返回 (r,g,b) 三元组
"""
with Image.open(image_path) as file:
size = file.size
# 确定区域
x1 = area[0] % size[0]
x2 = area[1] % size[0]
y1 = area[2] % size[1]
y2 = area[3] % size[1]
if x2 == 0:
x2 = size[0]
if y2 == 0:
y2 = size[1]
if x1 > x2:
t = x1
x1 = x2
x2 = t
if y1 > y2:
t = y1
y1 = y2
y2 = t
if x1 == x2:
if x2 != size[0]:
x2 += 1
else:
x1 -= 1
if y1 == y2:
if y2 != size[1]:
y2 += 1
else:
y1 -= 1
# 算出区域内所有像素点的平均值
img = np.asarray(file)
r = g = b = 0
count = 0
# 有没有透明度?
if len(img.shape) < 3:
pass
elif img.shape[2] == 4:
for y in range(y1, y2):
for x in range(x1, x2):
if img[y][x][3] > 0:
r += img[y][x][0]
g += img[y][x][1]
b += img[y][x][2]
count += 1
else:
for y in range(y1, y2):
for x in range(x1, x2):
r += img[y][x][0]
g += img[y][x][1]
b += img[y][x][2]
count += 1
if count == 0:
count = 1
# https://github.com/fkxxyz/ssfconv/issues/20
r = int(r)
g = int(g)
b = int(b)
r //= count
g //= count
b //= count
return (r, g, b)
# 获取图片大小的函数
def getImageSize(image_file:Path):
with Image.open(image_file) as file:
size = file.size
assert size[0] > 0 and size[0] < 65536 and size[1] > 0 and size[1] < 65536
return size
# 保存一个多边形到文件
def savePolygon(size, points, color, out_file:Path):
img = Image.new("RGBA", size)
draw = ImageDraw.Draw(img)
draw.polygon(points, fill=color)
img.save(out_file)
def rgbDistSqure(c1, c2):
"""
简单的计算两个颜色之间的距离
"""
dr = c1[0] - c2[0]
dg = c1[1] - c2[1]
db = c1[2] - c2[2]
return dr * dr + dg * dg + db * db
def rgbDistMax(color, *colors):
"""
求 colors 中与 color 的距离最大的颜色
"""
max_dist = 0
max_dist_color = colors[0]
for c in colors:
# basic-colormath 的距离计算方法更精确,但这一切值得吗
cur_dist = rgbDistSqure(color, c)
if max_dist < cur_dist:
max_dist = cur_dist
max_dist_color = c
return max_dist_color
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1744980332.0
ssfconv-1.2.2/src/ssfconv/convert/ini_ssf_operation.py 0000664 0001750 0001750 00000014014 15000444554 022310 0 ustar 00jimmy jimmy from .CaseSensitiveConfigParser import CaseSensitiveConfigParser
from .image_operation import *
import io
from pathlib import Path
import sys
from ssfconv.translation import _
default_menu_img_bin = io.BytesIO(
b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00<\x00\x00\x00<\x08\x06\x00\x00\x00:\xfc\xd9r\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\x00\x00\x00\tpHYs\x00\x00\x1b\xaf\x00\x00\x1b\xaf\x01^\x1a\x91\x1c\x00\x00\x00\x19tEXtSoftware\x00www.inkscape.org\x9b\xee<\x1a\x00\x00\x01\xdaIDATh\x81\xed\x9b\xc1N\xdb@\x10@\xdf\x80\x15R\x08R\x10\xa4\x12\xbd\x84\x037\xbe\xa0\x9f\xd2/\xec'\xf0\t|\x04\x1c\xc8\t\t\x88\x9a\x127AHdz\x98\xdd\xd6\nI$\xa4\x10\xd8a\x9e\xb4\xb2\xec\xb5\xady\x1e{}\x99\x11U\xa5\x89\x88\xec\x02-@(\x1b\x05\x9eTu\xd2<(Y8\x89\x1e\x02\xfb@\x1b\x1f\xc2\x8f\xc0\x18\x18f\xf1\n\xfe\xc9\x1e\x03_\x81\x03\xe0\x0b>\x84\xa7\xc0/\xa0%\"7\xaa:\xa9\xd2\xe4!&\xfb\r\xe8\x01\x1d`\xeb]\xc2\\\x1f3\xa0\xc6\x92\x07\xf0\x04L\xaa\x94\xdd},\xb3=L\xfa\x18\xf8\x0e\xf41\xf9\x92\xa8\x81\x01p\x01\xdc\xa4cS\xe0\xb7\x88\xecV\xd8\x02\xd5\xc6\x9eD\x07\x93\xfd\x01\xecm>\xd6\xb5\xd0\x01\xce\x80\x13\xe0'\xf0\x80\xb9\xb5\x81V\x85}\xabyla\x99\xdd\x03\xae\x80.\xf6j\xe8\xfc]?(\xd9a\x04\x9cb.\x974\x1c\xab\x05\x17\xf5\xd3\xb6\x0b[+\xde_\xdfNz_\xa1?jm\x00\x00\x00\x00IEND\xaeB`\x82"
)
default_radio_img_bin = io.BytesIO(
b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x18\x00\x00\x00\x18\x08\x06\x00\x00\x00\xe0w=\xf8\x00\x00\x00\tpHYs\x00\x00\r\xd7\x00\x00\r\xd7\x01B(\x9bx\x00\x00\x00\x19tEXtSoftware\x00www.inkscape.org\x9b\xee<\x1a\x00\x00\x00\x9dIDATH\x89\xed\x91\xc1\n\x830\x10Dg\x1a\xd0\x1f*\xbdfIoJ?\xb7-\xeda!G\xc1\x1fR\x88\xdb\x8b\xd2\x1e\x84\x08zk\xde1\xbb\x93\x07\xb3@\xa1P\xe0\xd6EUm\x9dsg\x00H)\xf5!\x84\xfba\x82\x18\xe3\xcb\xcc\x04@5?\r$\xd5{\xdf\xe4\xb2\xa7\xdc\x82\xaa\xb6f\xe6\x7f>\x07\x80\xda\xcc\xae1\xc6\xfd\x82\xb9\x96zeTM\xd3t\xd9-\xd8KV\x90R\xeaI\x8e+\xa3\x81d\x97\xcbo=\xf2\xc3\xcc\x02\xbeU\x8d\x00\xde"r;D0K\x9a\xa5s\x92\x9d\x88<\xb7f\x0b\x85\x7f\xe7\x03\xc2\x8b7\xa7\xab\xe8\x14\xb1\x00\x00\x00\x00IEND\xaeB`\x82'
)
class SsfIniWrapper:
def __init__(self, skin_dir: Path):
self.ssf = None
self.skin_dir: Path = skin_dir
self.read_ssf_ini()
def read_ssf_ini(self):
"""
为了重复使用,不和init合并
"""
skin_ini = self.skin_dir / "skin.ini"
if not skin_ini.is_file():
sys.stderr.write(_("Cant found skin.ini\n"))
return 1
try:
ssf_ini = CaseSensitiveConfigParser(allow_no_value=True)
ssf_ini.read(skin_ini, encoding="utf-16")
except:
sys.stderr.write(_("Failed to read skin.ini\n"))
return 2
self.ssf = ssf_ini
return
def get_image_config(self, section, key, index=0):
"""
获取图片文件名的函数(获取失败则返回空字符串)
"""
image_name_list = self.ssf[section].get(key, "").split(",")
if index < len(image_name_list):
image_name = image_name_list[index]
if (self.skin_dir / image_name).is_file():
return image_name
else:
return ""
def try_get_value(self, section, key):
return self.ssf[section].get(key, "").strip()
def getRawIni(self):
# FIXME 允许写这个变量是坏主意,考虑使用私有变量或私有方法来暗示
return self.ssf
def findBackgroundColorBy(self, key):
# 排除键值不存在
image_name = self.get_image_config(key[0], key[1])
if not image_name:
return None
# 排除区域不存在
h_str = self.try_get_value(key[0], key[1][:-3] + "layout_horizontal")
if not h_str:
return None
v_str = self.try_get_value(key[0], key[1][:-3] + "layout_vertical")
if not v_str:
return None
# 得出区域
h = h_str.split(",")
v = v_str.split(",")
if len(h) != 3 or len(v) != 3:
return None
# 排除平铺模式(筛选出是拉伸区域)
# if int(h[0]) != 0 or int(v[0]) != 0:
# return None
return getImageAvg(
self.skin_dir / image_name,
(int(h[1]), -int(h[2]), int(v[1]), -int(v[2])),
)
def findBackgroundColor(self):
"""
根据里面所有的图片,根据所设置的拉伸区域确定合适的背景色
不知道原作者为什么按顺序取到任意一张就完成
"""
keys = (
("Scheme_V1", "pic"),
("Scheme_V2", "pinyin_pic"),
("Scheme_V2", "zhongwen_pic"),
("Scheme_H1", "pic"),
("Scheme_H2", "pinyin_pic"),
("Scheme_H2", "zhongwen_pic"),
)
for key in keys:
avg_color = self.findBackgroundColorBy(key)
if avg_color:
return avg_color
else:
return (0, 0, 0)
# 将 skin.ini 的颜色转换成 (r,g,b) 三元组
def colorConv(ssf_color):
color_int = int(ssf_color, 16)
r = color_int % 256
g = (color_int % 65536) // 256
b = color_int // 65536
return (r, g, b)
def try_conv_to_int_tuple(val: str, fallback=None) -> tuple:
# 或许变成类方法,一步到位调用 CaseSensitiveConfigParser[section].get 更好,少一次判空
if val:
val = tuple(map(lambda s: int(s), val.split(",")))
else:
val = fallback
return val
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1744983866.5358224
ssfconv-1.2.2/src/ssfconv/convert/out/ 0000775 0001750 0001750 00000000000 15000453473 017033 5 ustar 00jimmy jimmy ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1744983837.0
ssfconv-1.2.2/src/ssfconv/convert/out/Fcitx.py 0000664 0001750 0001750 00000031731 15000453435 020465 0 ustar 00jimmy jimmy from ..image_operation import *
from ..ini_ssf_operation import *
from pathlib import Path
import os
# 创建符号链接的函数(若存在则覆盖)
def symlinkF(src, dst):
if os.path.isfile(dst):
os.remove(dst)
return os.symlink(src, dst)
def makeConfFromSsf(ssf):
skin = CaseSensitiveConfigParser(allow_no_value = True)
skin['SkinInfo'] = {
# 皮肤名称
'Name': ssf['General']['skin_name'],
# 皮肤版本
'Version': ssf['General']['skin_version'],
# 皮肤作者
'Author': ssf['General']['skin_author'],
# 描述
'Desc': ssf['General']['skin_info'],
}
return skin
def ssf2fcitx(skin_dir:Path):
"""
转换为 fcitx 格式
将解压后的 ssf 皮肤,在里面创建出 fcitx_skin.conf
"""
ssfw = SsfIniWrapper(skin_dir)
ssf=ssfw.getRawIni()
skin=makeConfFromSsf(ssf)
# 输入框输入的拼音颜色
input_color = colorConv(ssf['Display']['pinyin_color'])
# 列表中第一个词的颜色
first_color = colorConv(ssf['Display']['zhongwen_first_color'])
# 列表中其他词的颜色
other_color = colorConv(ssf['Display']['zhongwen_color'])
# 根据里面所有的图片,根据所设置的拉伸区域确定合适的背景色
back_color = ssfw.findBackgroundColor()
# 字体大小(像素)
font_size = int(ssf['Display']['font_size'])
# 状态栏背景图
static_bar_image = ssfw.get_image_config('StatusBar', 'pic')
# 中/英文状态
cn_status_image = ssfw.get_image_config('StatusBar', 'cn_en', 0)
en_status_image = ssfw.get_image_config('StatusBar', 'cn_en', 1)
# 全半角状态
quan_status_image = ssfw.get_image_config('StatusBar', 'quan_ban', 0)
ban_status_image = ssfw.get_image_config('StatusBar', 'quan_ban', 1)
# 中/英文标点状态
cn_p_status_image = ssfw.get_image_config('StatusBar', 'biaodian', 0)
en_p_status_image = ssfw.get_image_config('StatusBar', 'biaodian', 1)
# 繁/简状态
simp_status_image = ssfw.get_image_config('StatusBar', 'fan_jian', 1)
trad_status_image = ssfw.get_image_config('StatusBar', 'fan_jian', 0)
# 虚拟键盘状态
vk_inactive_status_image = ssfw.get_image_config('StatusBar', 'softkeyboard')
for mouse_status in ('down','in','out','downing'):
vk_active_status_image = ssfw.get_image_config('StatusBar', 'softkeyboard_' + mouse_status)
if vk_active_status_image:
break
icons = (cn_status_image, simp_status_image, trad_status_image,
quan_status_image, ban_status_image,
cn_p_status_image, en_p_status_image,
vk_inactive_status_image, vk_active_status_image)
# 求图标的前景色(任意一个即可)
for image in icons:
if image:
icon_color = getImageAvg(skin_dir / image)
break
else:
icon_color = other_color
skin['SkinFont'] = {
# 字体大小
'FontSize': font_size,
# 菜单字体大小
'MenuFontSize': 14,
# 字体大小遵守dpi设置
'RespectDPI': 'False',
# 提示信息颜色
'TipColor': '%d %d %d' % first_color,
# 输入信息颜色
'InputColor': '%d %d %d' % other_color,
# 候选词索引颜色
'IndexColor': '%d %d %d' % other_color,
# 第一候选词颜色
'FirstCandColor': '%d %d %d' % first_color,
# 用户词组颜色
'UserPhraseColor': '%d %d %d' % first_color,
# 码表提示颜色
'CodeColor': '%d %d %d' % input_color,
# 其他颜色
'OtherColor': '%d %d %d' % other_color,
# 活动菜单项颜色
'ActiveMenuColor': '%d %d %d' % \
rgbDistMax(other_color,
first_color, input_color, back_color, icon_color),
# 非活动菜单项颜色+状态栏图标文字颜色
'InactiveMenuColor': '%d %d %d' % \
rgbDistMax(back_color,
first_color, input_color, other_color, icon_color),
}
# 创建中文拼音状态图 pinyin.png
if cn_status_image:
symlinkF(cn_status_image, skin_dir / 'pinyin.png')
# 创建全/半角状态图 fullwidth_active.png / fullwidth_inactive.png
if quan_status_image:
symlinkF(quan_status_image, skin_dir / 'fullwidth_active.png')
if ban_status_image:
symlinkF(ban_status_image, skin_dir / 'fullwidth_inactive.png')
# 创建中/英文标点状态图 punc_active.png / punc_inactive.png
if cn_p_status_image:
symlinkF(cn_p_status_image, skin_dir / 'punc_active.png')
if en_p_status_image:
symlinkF(en_p_status_image, skin_dir / 'punc_inactive.png')
# 创建繁/简状态图 chttrans_inactive.png / chttrans_active.png
if simp_status_image:
symlinkF(simp_status_image, skin_dir / 'chttrans_inactive.png')
if trad_status_image:
symlinkF(trad_status_image, skin_dir / 'chttrans_active.png')
# 创建虚拟键盘状态图 vk_inactive.png / vk_active.png
if vk_inactive_status_image:
symlinkF(vk_inactive_status_image, skin_dir / 'vk_inactive.png')
if vk_active_status_image:
symlinkF(vk_active_status_image, skin_dir / 'vk_active.png')
# 求搜狗状态栏上几个按钮的坐标的最值
x_min = y_min = 65536
x_max = y_max = 0
for button in ('cn_en',
'biaodian',
'quan_ban',
'quan_shuang',
'fan_jian',
'softkeyboard',
'menu',
'sogousearch',
'passport',
'skinmanager'):
display = ssfw.try_get_value('StatusBar', button + '_display')
if display != '1': continue
pos = ssfw.try_get_value('StatusBar', button + '_pos').split(',')
if len(pos) != 2: continue
# 取最值
if int(pos[0]) < x_min: x_min = int(pos[0])
if int(pos[1]) < y_min: y_min = int(pos[1])
# 得到图标尺寸
icon_image = ssfw.get_image_config('StatusBar', button, 0)
if not icon_image: continue
size = getImageSize(skin_dir / icon_image)
# 取最右值
x = int(pos[0]) + size[0]
if x > x_max: x_max = x
y = int(pos[1]) + size[1]
if y > y_max: y_max = y
# 得出合适的右边距和下边距
if static_bar_image:
size = getImageSize(skin_dir / static_bar_image)
MarginRight = size[0] - x_max + 4
MarginBottom = size[1] - y_max + 4
else:
MarginRight = 4
MarginBottom = 4
skin['SkinMainBar'] = {
# 背景图片
'BackImg': static_bar_image,
# Logo图标
'Logo': '',
# 英文模式图标
'Eng': en_status_image,
# 激活状态输入法图标
'Active': cn_status_image,
# 左边距
'MarginLeft': x_min+4,
# 右边距
'MarginRight': MarginRight,
# 上边距
'MarginTop': y_min+4,
# 下边距
'MarginBottom': MarginBottom,
# 可点击区域的左边距
#ClickMarginLeft=0
# 可点击区域的右边距
#ClickMarginRight=0
# 可点击区域的上边距
#ClickMarginTop=0
# 可点击区域的下边距
#ClickMarginBottom=0
# 覆盖图片
#Overlay=
# 覆盖图片停靠位置
# Available Value:
# TopLeft
# TopCenter
# TopRight
# CenterLeft
# Center
# CenterRight
# BottomLeft
# BottomCenter
# BottomRight
#OverlayDock=TopLeft
# 覆盖图片 X 偏移
#OverlayOffsetX=0
# 覆盖图片 Y 偏移
#OverlayOffsetY=0
# 纵向填充规则
# Available Value:
# Copy
# Resize
#FillVertical=Resize
# 横向填充规则
# Available Value:
# Copy
# Resize
#FillHorizontal=Resize
# 使用自定的文本图标颜色
# Available Value:
# True False
#UseCustomTextIconColor=True
# 活动的文本图标颜色
#ActiveTextIconColor=101 153 209
# 非活动的文本图标颜色
#InactiveTextIconColor=101 153 209
# 特殊图标位置
#Placement=
}
# 输入框背景图
input_bar_image = ssfw.get_image_config('Scheme_H1', 'pic')
input_bar_image_size = getImageSize(skin_dir / input_bar_image)
# 绘制 prev.png 和 next.png 颜色为 '%d %d %d' % other_color
savePolygon((6,12), ((0,0),(6,6),(0,12)), other_color, skin_dir / 'next.png')
savePolygon((6,12), ((0,6),(6,0),(6,12)), other_color, skin_dir / 'prev.png')
# 水平边距
lh = ssfw.try_get_value('Scheme_H1', 'layout_horizontal')
lh = try_conv_to_int_tuple(lh,(0, 0, 0))
# 竖直边距
pinyin_marge = ssfw.try_get_value('Scheme_H1', 'pinyin_marge')
pinyin_marge = try_conv_to_int_tuple(pinyin_marge)
if pinyin_marge:
pass
else:
assert False
zhongwen_marge = ssfw.try_get_value('Scheme_H1', 'zhongwen_marge')
zhongwen_marge = try_conv_to_int_tuple(zhongwen_marge)
if zhongwen_marge:
pass
else:
assert False
separator = ssfw.try_get_value('Scheme_H1', 'separator')
sep = 1 if separator else 0
InputPos = pinyin_marge[0] + font_size
OutputPos = pinyin_marge[0] + pinyin_marge[1] + font_size + \
sep + zhongwen_marge[0] + font_size
MarginBottom = input_bar_image_size[1] - OutputPos
if lh[1] - pinyin_marge[2] > 32:
MarginLeft = pinyin_marge[2]
else:
MarginLeft = lh[1]
skin['SkinInputBar'] = {
# 背景图片
'BackImg': input_bar_image,
# 左边距
'MarginLeft': MarginLeft,
# 右边距
'MarginRight': lh[2],
# 上边距
'MarginTop': 0,
# 下边距
'MarginBottom': MarginBottom,
# 可点击区域的左边距
#ClickMarginLeft=0
# 可点击区域的右边距
#ClickMarginRight=0
# 可点击区域的上边距
#ClickMarginTop=0
# 可点击区域的下边距
#ClickMarginBottom=0
# 覆盖图片
#Overlay=hangul.png
# 覆盖图片停靠位置
# Available Value:
# TopLeft
# TopCenter
# TopRight
# CenterLeft
# Center
# CenterRight
# BottomLeft
# BottomCenter
# BottomRight
#OverlayDock=TopRight
# 覆盖图片 X 偏移
#OverlayOffsetX=-26
# 覆盖图片 Y 偏移
#OverlayOffsetY=2
# 光标颜色
'CursorColor': '%d %d %d' % first_color,
# 预编辑文本的位置或偏移
'InputPos': InputPos,
# 候选词表的位置或偏移
'OutputPos': OutputPos,
# 上一页图标
'BackArrow': 'prev.png',
# 下一页图标
'ForwardArrow': 'next.png',
# 上一页图标的横坐标
'BackArrowX': lh[2] - lh[1] + 10,
# 上一页图标的纵坐标
'BackArrowY': pinyin_marge[0],
# 下一页图标的横坐标
'ForwardArrowX': lh[2] - lh[1],
# 下一页图标的纵坐标
'ForwardArrowY': pinyin_marge[0],
# 纵向填充规则
# Available Value:
# Copy
# Resize
#FillVertical=Resize
# 横向填充规则
# Available Value:
# Copy
# Resize
#FillHorizontal=Resize
}
# 使用系统默认的 active.png 和 inactive.png
symlinkF('/usr/share/fcitx/skin/default/active.png',
skin_dir / 'active.png')
symlinkF('/usr/share/fcitx/skin/default/inactive.png',
skin_dir / 'inactive.png')
skin['SkinTrayIcon'] = {
# 活动输入法图标
'Active': 'active.png',
# 非活动输入法图标
'Inactive': 'inactive.png',
}
# 用纯背景色构建出本主题的 menu.png
img = Image.open(default_menu_img_bin)
a = np.array(img)
for i in range(len(a)):
for j in range(len(a[0])):
if a[i][j][3]:
a[i][j][0] = back_color[0]
a[i][j][1] = back_color[1]
a[i][j][2] = back_color[2]
img = Image.fromarray(a)
img.save(skin_dir / 'menu.png')
skin['SkinMenu'] = {
# 背景图片
'BackImg': 'menu.png',
# 上边距
'MarginTop': 8,
# 下边距
'MarginBottom': 8,
# 左边距
'MarginLeft': 8,
# 右边距
'MarginRight': 8,
# 活动菜单项颜色
'ActiveColor': '%d %d %d' % other_color,
# 分隔线颜色
'LineColor': '%d %d %d' % other_color,
}
skin['SkinKeyboard'] = {
# 虚拟键盘图片
#BackImg=keyboard.png
# 软键盘按键文字颜色
#'KeyColor': '%d %d %d' % first_color,
}
skin.write(open(skin_dir / 'fcitx_skin.conf', 'w', encoding="utf-8"), False)
return 0 ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1744983664.0
ssfconv-1.2.2/src/ssfconv/convert/out/Fcitx5.py 0000664 0001750 0001750 00000023677 15000453160 020557 0 ustar 00jimmy jimmy from ..ini_ssf_operation import *
from pathlib import Path
def makeConfFromSsf(ssf):
skin = CaseSensitiveConfigParser(allow_no_value = True)
skin['Metadata'] = {
# 皮肤名称
'Name': ssf['General']['skin_name'],
# 皮肤版本
'Version': ssf['General']['skin_version'],
# 皮肤作者
'Author': ssf['General']['skin_author'],
# 描述
'Description': ssf['General']['skin_info'],
# 用 DPI 缩放
'ScaleWithDPI': 'False',
}
return skin
def ssf2fcitx5(skin_dir:Path):
"""
转换为 fcitx5 格式
将解压后的 ssf 皮肤,在里面创建出 theme.conf
"""
ssfw = SsfIniWrapper(skin_dir)
ssf=ssfw.getRawIni()
skin=makeConfFromSsf(ssf)
# 输入框(pre_edit)输入的拼音颜色
input_color = colorConv(ssf['Display']['pinyin_color'])
# 列表中第一个词的颜色
first_color = colorConv(ssf['Display']['zhongwen_first_color'])
# 列表中其他词的颜色
other_color = colorConv(ssf['Display']['zhongwen_color'])
# 根据里面所有的图片,根据所设置的拉伸区域确定合适的背景色
back_color = ssfw.findBackgroundColor()
# 字体大小(像素)
font_size = int(ssf['Display']['font_size'])
skin['InputPanel'] = {
# 字体及其大小
'Font': 'Sans %d' % font_size,
# 非选中候选字颜色
'NormalColor': '#%02x%02x%02x' % other_color,
# 选中候选字颜色
'HighlightCandidateColor': '#%02x%02x%02x' % first_color,
# 高亮前景颜色(back_color输入字符颜色)
'HighlightColor': '#%02x%02x%02x' % input_color,
# 输入字符背景颜色
'HighlightBackgroundColor': '#%02x%02x%02x' % back_color,
#
'Spacing': 3,
}
# 输入框背景图
input_bar_image = ssfw.get_image_config('Scheme_H1', 'pic')
input_bar_image_size = getImageSize(skin_dir / input_bar_image)
# 水平拉升区域
lh = ssfw.try_get_value('Scheme_H1', 'layout_horizontal')
lh = try_conv_to_int_tuple(lh,(0, 2, 2))
# 垂直拉升区域
lv = ssfw.try_get_value('Scheme_H1', 'layout_vertical')
lv = try_conv_to_int_tuple(lv,(0, 2, 2))
# 拼音边距
pinyin_marge = ssfw.try_get_value('Scheme_H1', 'pinyin_marge')
pinyin_marge = try_conv_to_int_tuple(pinyin_marge)
if pinyin_marge:
pass
else:
assert False
# 候选词边距
zhongwen_marge = ssfw.try_get_value('Scheme_H1', 'zhongwen_marge')
zhongwen_marge = try_conv_to_int_tuple(zhongwen_marge)
if zhongwen_marge:
pass
else:
assert False
# 分隔符长度
sep = 1 if ssfw.try_get_value('Scheme_H1', 'separator') else 0
# 恒等式:
# 输入的拼音下方到候选词上方的距离:
# pinyin_marge[1] + sep + zhongwen_marge[0] = TextMargin.Bottom + TextMargin.Top
# 输入的拼音上方到上方边界的距离:
# pinyin_marge[0] = ContentMargin.Top + TextMargin.Top
# 候选词下方到下方边界的距离:
# zhongwen_marge[1] = ContentMargin.Bottom + TextMargin.Bottom
#
#
# 这是四元一次方程组,由于只有三个方程,那么随便确定其中一个即可解得其它未知数。
# 增加的方程:
# TextMargin.Bottom = (pinyin_marge[1] + sep + zhongwen_marge[0]) // 2
distant_pinyin_zhongwen = pinyin_marge[1] + sep + zhongwen_marge[0]
# 解得:
TextMargin_Bottom = distant_pinyin_zhongwen // 2
TextMargin_Top = distant_pinyin_zhongwen - TextMargin_Bottom
ContentMargin_Top = pinyin_marge[0] - TextMargin_Top
#ContentMargin_Bottom = zhongwen_marge[1] - TextMargin_Bottom
ContentMargin_Bottom = input_bar_image_size[1] - \
ContentMargin_Top - TextMargin_Top - font_size - TextMargin_Bottom - \
TextMargin_Top - font_size - TextMargin_Bottom
TextMargin_Top_Left = 5
TextMargin_Top_Right = 5
# 文字边距
skin['InputPanel/TextMargin'] = {
'Left': TextMargin_Top_Left,
'Right': TextMargin_Top_Right,
'Top': TextMargin_Top,
'Bottom': TextMargin_Bottom,
}
# 输入框内容边距
skin['InputPanel/ContentMargin'] = {
'Left': max(pinyin_marge[2], zhongwen_marge[2]) - TextMargin_Top_Left,
'Right': max(pinyin_marge[3], zhongwen_marge[3]) - TextMargin_Top_Right,
'Top': ContentMargin_Top,
'Bottom': ContentMargin_Bottom,
}
# 输入框背景图
skin['InputPanel/Background'] = {
'Image': input_bar_image,
}
# 输入框背景图的拉升区域
skin['InputPanel/Background/Margin'] = {
'Left': lh[1],
'Right': lh[2],
'Top': lv[1],
'Bottom': lv[2],
}
# 绘制高亮的纯色图片
# menu_highlight_color = rgbDistMax(first_color, input_color, other_color, back_color)
Image.new('RGBA', (38,23), (0,0,0,0)).save(skin_dir / 'highlight.png')
# 高亮背景
skin['InputPanel/Highlight'] = {
'Image': 'highlight.png',
}
# 高亮背景边距
skin['InputPanel/Highlight/Margin'] = {
'Left': 5,
'Right': 5,
'Top': 5,
'Bottom': 5,
}
# 绘制 prev.png 和 next.png 颜色为 '%d %d %d' % other_color
savePolygon((16,24), ((5,6),(5,18),(11,12)), other_color, skin_dir / 'next.png')
savePolygon((16,24), ((11,6),(11,18),(5,12)), other_color, skin_dir / 'prev.png')
# 前一页的箭头
skin['InputPanel/PrevPage'] = {
'Image': 'prev.png',
}
skin['InputPanel/PrevPage/ClickMargin'] = {
'Left': 5,
'Right': 5,
'Top': 4,
'Bottom': 4,
}
# 后一页的箭头
skin['InputPanel/NextPage'] = {
'Image': 'next.png',
}
skin['InputPanel/NextPage/ClickMargin'] = {
'Left': 5,
'Right': 5,
'Top': 4,
'Bottom': 4,
}
# 竖排合窗口设置
Scheme_V1_pic = ssfw.try_get_value('Scheme_V1', 'pic')
# 水平拉升区域
lh = ssfw.try_get_value('Scheme_V1', 'layout_horizontal')
lh = try_conv_to_int_tuple(lh)
# 垂直拉升区域
lv = ssfw.try_get_value('Scheme_V1', 'layout_vertical')
lv = try_conv_to_int_tuple(lv)
# 拼音边距
pinyin_marge = ssfw.try_get_value('Scheme_V1', 'pinyin_marge')
pinyin_marge = try_conv_to_int_tuple(pinyin_marge)
# 候选词边距
zhongwen_marge = ssfw.try_get_value('Scheme_V1', 'zhongwen_marge')
zhongwen_marge = try_conv_to_int_tuple(zhongwen_marge)
if Scheme_V1_pic and lh and lv and pinyin_marge and zhongwen_marge:
# 背景图片
skin['Menu/Background'] = {
'Image': Scheme_V1_pic,
}
# 背景图片拉升边距
skin['Menu/Background/Margin'] = {
'Left': lh[1],
'Right': lh[2],
'Top': lv[1],
'Bottom': lv[2],
}
sep = 1 if ssfw.try_get_value('Scheme_V1', 'separator') else 0
# 背景图片内容边距
horizontal_margin = min(zhongwen_marge[2], zhongwen_marge[3])
skin['Menu/ContentMargin'] = {
# 左边距
'Left': horizontal_margin,
# 右边距
'Right': horizontal_margin,
# 上边距
'Top': pinyin_marge[0] + pinyin_marge[1] + sep + zhongwen_marge[0],
# 下边距
'Bottom': zhongwen_marge[1],
}
else:
# 构建纯色背景
# 用纯背景色构建出本主题的 menu.png
img = Image.open(default_menu_img_bin)
a = np.array(img)
for i in range(len(a)):
for j in range(len(a[0])):
if a[i][j][3]:
a[i][j][0] = back_color[0]
a[i][j][1] = back_color[1]
a[i][j][2] = back_color[2]
img = Image.fromarray(a)
img.save(skin_dir / 'menu.png')
# 背景图片
skin['Menu/Background'] = {
'Image': 'menu.png',
}
# 背景图片拉升边距
skin['Menu/Background/Margin'] = {
'Left': 20,
'Right': 20,
'Top': 20,
'Bottom': 20,
}
# 背景图片内容边距
skin['Menu/ContentMargin'] = {
# 左边距
'Left': 8,
# 右边距
'Right': 8,
# 上边距
'Top': 8,
# 下边距
'Bottom': 8,
}
# 绘制高亮的透明图片
#menu_highlight_color = rgbDistMax((255,255,255), back_color, input_color, first_color, other_color)
Image.new('RGBA', (38,23), (0,0,0,0)).save(skin_dir / 'menu_highlight.png')
# 高亮背景
skin['Menu/Highlight'] = {
'Image': 'menu_highlight.png',
}
# 高亮背景边距
skin['Menu/Highlight/Margin'] = {
'Left': 10,
'Right': 10,
'Top': 5,
'Bottom': 5,
}
# 分隔符颜色
skin['Menu/Separator'] = {
'Color': '#%02x%02x%02x' % other_color,
}
# 用纯背景色构建出本主题的 radio.png
img = Image.open(default_radio_img_bin)
a = np.array(img)
for i in range(len(a)):
for j in range(len(a[0])):
if a[i][j][3]:
a[i][j][0] = other_color[0]
a[i][j][1] = other_color[1]
a[i][j][2] = other_color[2]
img = Image.fromarray(a)
img.save(skin_dir / 'radio.png')
# 复选框图片
skin['Menu/CheckBox'] = {
'Image': 'radio.png',
}
# 绘制箭头图片
savePolygon((6,12), ((0,0),(6,6),(0,12)), other_color, skin_dir / 'arrow.png')
# 箭头图片
skin['Menu/SubMenu'] = {
'Image': 'arrow.png',
}
# 菜单文字项边距
skin['Menu/TextMargin'] = {
# 左边距
'Left': 5,
# 右边距
'Right': 5,
# 上边距
'Top': 5,
# 下边距
'Bottom': 5,
}
skin.write(open(skin_dir / 'theme.conf', 'w', encoding="utf-8"), False)
return 0
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1742720619.0
ssfconv-1.2.2/src/ssfconv/convert/out/__init__.py 0000664 0001750 0001750 00000000133 14767747153 021165 0 ustar 00jimmy jimmy __all__ = ["Fcitx", "Fcitx5"]
from .Fcitx import ssf2fcitx
from .Fcitx5 import ssf2fcitx5
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1744983866.5368226
ssfconv-1.2.2/src/ssfconv/extract/ 0000775 0001750 0001750 00000000000 15000453473 016216 5 ustar 00jimmy jimmy ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1742720619.0
ssfconv-1.2.2/src/ssfconv/extract/__init__.py 0000664 0001750 0001750 00000000002 14767747153 020343 0 ustar 00jimmy jimmy
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1742913469.0
ssfconv-1.2.2/src/ssfconv/extract/ssf.py 0000664 0001750 0001750 00000003660 14770537675 017414 0 ustar 00jimmy jimmy from Cryptodome.Cipher import AES
import zlib, struct
from pathlib import Path
import zipfile
# 这部分最早可以追溯到 https://github.com/KDE/kimtoy/blob/master/kssf.cpp
def extract_ssf(file_path:Path, dest_dir:Path):
"""
解压ssf文件到指定文件夹,文件夹不存在会自动创建
ssf 文件格式目前有两种,一种是加密过后,一种未加密的zip
"""
def __decrypt(bin):
# AES 解密内容
aesKey = b'\x52\x36\x46\x1A\xD3\x85\x03\x66' + \
b'\x90\x45\x16\x28\x79\x03\x36\x23' + \
b'\xDD\xBE\x6F\x03\xFF\x04\xE3\xCA' + \
b'\xD5\x7F\xFC\xA3\x50\xE4\x9E\xD9'
iv = b'\xE0\x7A\xAD\x35\xE0\x90\xAA\x03' + \
b'\x8A\x51\xFD\x05\xDF\x8C\x5D\x0F'
ssfAES = AES.new(aesKey, AES.MODE_CBC, iv)
plain_bin = ssfAES.decrypt(bin[8:])
# zlib 解压内容
data = zlib.decompress(plain_bin[4:]) # 注意要跳过头四字节
def readUint(offset):
return struct.unpack('I', data[offset:offset+4])[0]
# 整个内容的大小
size = readUint(0)
# 得到若干个偏移量
offsets_size = readUint(4)
offsets = struct.unpack('I'*(offsets_size//4),data[8:8+offsets_size])
for offset in offsets:
# 得到文件名
name_len = readUint(offset)
filename = data[offset+4:offset+4+name_len].decode('utf-16')
# 得到文件内容
content_len = readUint(offset+4+name_len)
content = data[offset+8+name_len:offset+8+name_len+content_len]
(dest_dir / filename).write_bytes(content)
return
ssf_bin = file_path.read_bytes()
if ssf_bin[:4] == b'Skin': # 通过头四字节判断是否被加密
__decrypt(ssf_bin)
else:
# 直接 zip 解压
with zipfile.ZipFile(file_path) as zf:
zf.extractall(dest_dir) ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1743300678.0
ssfconv-1.2.2/src/ssfconv/translation.py 0000664 0001750 0001750 00000000564 14772124106 017464 0 ustar 00jimmy jimmy # import gettext, locale
# localedir = Path() / "data" / "locales"
# l10n = gettext.translation(
# "ssfconv",
# localedir=localedir,
# fallback=True,
# )
# l10n.install()
# _ = l10n.gettext
#BUG https://github.com/breezy-team/setuptools-gettext/issues/94
# I dont want to use deprecated setup.py so i18n attempt get stuck
_ = lambda x:x #gettext placeholder ././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1744983866.5368226
ssfconv-1.2.2/src/ssfconv.egg-info/ 0000775 0001750 0001750 00000000000 15000453473 016236 5 ustar 00jimmy jimmy ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1744983866.0
ssfconv-1.2.2/src/ssfconv.egg-info/PKG-INFO 0000644 0001750 0001750 00000011604 15000453472 017332 0 ustar 00jimmy jimmy Metadata-Version: 2.4
Name: ssfconv
Version: 1.2.2
Summary: Sogou input method skin file (.ssf file) converter
Author: nihui, VOID001, fkxxyz, RadND
License: GPL-3.0-or-later
Project-URL: Repository, https://codeberg.org/radnd/ssfconv.git
Project-URL: Issues, https://codeberg.org/radnd/ssfconv/issues
Keywords: ssf,fcitx,fcitx5,Fcitx5 Classic User interface
Classifier: Programming Language :: Python :: 3
Classifier: Operating System :: OS Independent
Classifier: Environment :: Console
Classifier: Intended Audience :: End Users/Desktop
Classifier: Natural Language :: Chinese (Simplified)
Classifier: Topic :: Adaptive Technologies
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: pycryptodomex
Requires-Dist: pillow
Requires-Dist: numpy
Dynamic: license-file
# 简介

一个将皮肤从搜狗格式转换为 fcitx/fcitx5 格式的脚本,如果您在寻找“原生”的fcitx5皮肤,可以看[这里](https://github.com/topics/fcitx5-theme)
新开了仓库,因为作者的账号很久没有活动过,我所做的改动也很大:
1. 制成 python 包,用户不需要自己装依赖了
2. 重构,减少重复,将原先的脚本拆分为解包和转换两部分
3. 注册命令行程序、调整命令行参数、中文本地化
4. 由于使用了match case语法,python版本要求上升到3.10,如果想用旧版本python运行,请自行把相关代码改回长串 if elif
程序的目的没有变,所以原作者的[参考图像](https://www.fkxxyz.com/d/ssfconv)依然适用
# 安装
## 手动
clone或下载release
```shell
python -m venv venv_name
source venv_name/bin/activate
# source venv_name/bin/activate.fish
pip install .
```
## pip
[ ] 下次一定上传到 pypi
```shell
pip install ssfconv
```
# 使用
本包提供的命令行程序加 `-h` 参数就可以查看帮助
## 获取皮肤
可以从[搜狗的皮肤站](https://pinyin.sogou.com/skins/)或bilibili搜索输入法皮肤获取自己喜欢的皮肤,得到ssf格式的文件,例如 【雨欣】蒲公英的思念.ssf
## 解包 ssf 皮肤
```shell
ssfconv unpack 【雨欣】蒲公英的思念.ssf
```
得到的文件夹可供 `ssfconv` 使用
## 转换为 fcitx4 皮肤
```shell
ssfconv convert -t fcitx 【雨欣】蒲公英的思念
```
复制到用户皮肤目录
```shell
mkdir -p ~/.config/fcitx/skin/
cp -r 【雨欣】蒲公英的思念 ~/.config/fcitx/skin/
```
右键输入法托盘图表,选中皮肤,这款皮肤是不是出现在列表里了呢,尽情享用吧。
## 转换为 fcitx5 主题
```shell
ssfconv convert 【雨欣】蒲公英的思念
```
复制到用户主题目录
```shell
mkdir -p ~/.local/share/fcitx5/themes/
cp -r 【雨欣】蒲公英的思念 ~/.local/share/fcitx5/themes/
```
打开 fcitx5 的配置,附加组件标签,经典用户界面,点配置,在主题的下拉列表里,选择这款皮肤。
或者你也可以直接修改配置文件 ~/.config/fcitx5/conf/classicui.conf,将 Theme 的值改成这个皮肤的名称即可。
查看该皮肤在配置文件中的名称:
```shell
grep Name ~/.local/share/fcitx5/themes/【雨欣】蒲公英的思念/theme.conf
```
# 微调
转换得到的皮肤配置或多或少会有点瑕疵,其实调整它们并不困难,快去试试吧
# 致谢
前两位作者的仓库分别在
- https://github.com/fkxxyz/ssfconv
- https://github.com/VOID001/ssf2fcitx
前两位作者的账号都不活动,也许已经对系统美化不感兴趣了。不管怎样,我把他们写在了 `pyproject.toml` 中,在此对他们表示感谢。( •ᴗ• )
还要感谢 [`那些时光.ssf`](https://pinyin.sogou.com/d/skins/download.php?skin_id=344544) 的作者 szwjerry ,为了用上这个皮肤,我才接触到这个工具。
# 贡献征集
## 方格示意
需要一个调试工具,能依据转换后的皮肤配置文件在图片上划线,用以表明这个皮肤是怎样被拉伸的
考虑到这和转换的过程无关,更像是一个独立的程序
## 更多整理
转换部分的主函数仍然非常长,需要拆分,有部分固定的配置可以抽离出来以模板文件的形式存在
## IBus
```
GNOME桌面和非GNOME桌面的IBus采用了两个不同的前端。非GNOME桌面的IBus是项目自己本身基于GTK写的一个很简陋的前端,其使用GTK主题,根据我之前的研究,要实现搜狗那样的皮肤效果应该不太可能。GNOME在他们的GJS代码库里给IBus重写了一个前端,他们的前端可定制性更强,主要是使用CSS文件指定样式
@HollowMan6
```
仅靠转换程序做不到在 IBus 上实现搜狗这种美观的皮肤效果,看样子最有希望的办法是通过 GNOME Shell 扩展修改输入法前端,先行提供背景图片等素材的显示和拉伸/压缩的能力
readme有点长,其他不重要的内容看 [Wiki 页面](https://github.com/RadND/ssfconv/wiki)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1744983866.0
ssfconv-1.2.2/src/ssfconv.egg-info/SOURCES.txt 0000664 0001750 0001750 00000001175 15000453472 020125 0 ustar 00jimmy jimmy LICENSE
README.md
pyproject.toml
src/ssfconv/__init__.py
src/ssfconv/cli.py
src/ssfconv/translation.py
src/ssfconv.egg-info/PKG-INFO
src/ssfconv.egg-info/SOURCES.txt
src/ssfconv.egg-info/dependency_links.txt
src/ssfconv.egg-info/entry_points.txt
src/ssfconv.egg-info/requires.txt
src/ssfconv.egg-info/top_level.txt
src/ssfconv/convert/CaseSensitiveConfigParser.py
src/ssfconv/convert/__init__.py
src/ssfconv/convert/image_operation.py
src/ssfconv/convert/ini_ssf_operation.py
src/ssfconv/convert/out/Fcitx.py
src/ssfconv/convert/out/Fcitx5.py
src/ssfconv/convert/out/__init__.py
src/ssfconv/extract/__init__.py
src/ssfconv/extract/ssf.py ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1744983866.0
ssfconv-1.2.2/src/ssfconv.egg-info/dependency_links.txt 0000664 0001750 0001750 00000000001 15000453472 022303 0 ustar 00jimmy jimmy
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1744983866.0
ssfconv-1.2.2/src/ssfconv.egg-info/entry_points.txt 0000664 0001750 0001750 00000000055 15000453472 021533 0 ustar 00jimmy jimmy [console_scripts]
ssfconv = ssfconv.cli:main
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1744983866.0
ssfconv-1.2.2/src/ssfconv.egg-info/requires.txt 0000664 0001750 0001750 00000000033 15000453472 020631 0 ustar 00jimmy jimmy pycryptodomex
pillow
numpy
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1744983866.0
ssfconv-1.2.2/src/ssfconv.egg-info/top_level.txt 0000664 0001750 0001750 00000000010 15000453472 020756 0 ustar 00jimmy jimmy ssfconv