././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744983866.5368226 ssfconv-1.2.2/0000775000175000017500000000000015000453473012274 5ustar00jimmyjimmy././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1742720619.0 ssfconv-1.2.2/LICENSE0000664000175000017500000000137314767747153013331 0ustar00jimmyjimmy 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". ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744983866.5368226 ssfconv-1.2.2/PKG-INFO0000644000175000017500000001160415000453473013371 0ustar00jimmyjimmyMetadata-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 # 简介 ![gplv3](./gplv3-with-text-136x68.png) 一个将皮肤从搜狗格式转换为 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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1742721317.0 ssfconv-1.2.2/README.md0000664000175000017500000001012314767750445013572 0ustar00jimmyjimmy# 简介 ![gplv3](./gplv3-with-text-136x68.png) 一个将皮肤从搜狗格式转换为 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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744982564.0 ssfconv-1.2.2/pyproject.toml0000664000175000017500000000167715000451044015213 0ustar00jimmyjimmy[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" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744983866.5368226 ssfconv-1.2.2/setup.cfg0000664000175000017500000000004615000453473014115 0ustar00jimmyjimmy[egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744983866.5328224 ssfconv-1.2.2/src/0000775000175000017500000000000015000453473013063 5ustar00jimmyjimmy././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744983866.5338223 ssfconv-1.2.2/src/ssfconv/0000775000175000017500000000000015000453473014544 5ustar00jimmyjimmy././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1742720619.0 ssfconv-1.2.2/src/ssfconv/__init__.py0000664000175000017500000000000214767747153016671 0ustar00jimmyjimmy ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744981473.0 ssfconv-1.2.2/src/ssfconv/cli.py0000775000175000017500000000645415000446741015701 0ustar00jimmyjimmyimport 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) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744983866.5358224 ssfconv-1.2.2/src/ssfconv/convert/0000775000175000017500000000000015000453473016224 5ustar00jimmyjimmy././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1742720619.0 ssfconv-1.2.2/src/ssfconv/convert/CaseSensitiveConfigParser.py0000664000175000017500000000030314767747153023666 0ustar00jimmyjimmyimport configparser # 为了使其区分大小写,重载 ConfigParser class CaseSensitiveConfigParser(configparser.ConfigParser): def optionxform(self, optionstr): return optionstr././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743303800.0 ssfconv-1.2.2/src/ssfconv/convert/__init__.py0000664000175000017500000000165314772132170020345 0ustar00jimmyjimmyfrom .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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744980605.0 ssfconv-1.2.2/src/ssfconv/convert/image_operation.py0000664000175000017500000000626615000445175021752 0ustar00jimmyjimmyfrom 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744980332.0 ssfconv-1.2.2/src/ssfconv/convert/ini_ssf_operation.py0000664000175000017500000001401415000444554022310 0ustar00jimmyjimmyfrom .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 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744983866.5358224 ssfconv-1.2.2/src/ssfconv/convert/out/0000775000175000017500000000000015000453473017033 5ustar00jimmyjimmy././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744983837.0 ssfconv-1.2.2/src/ssfconv/convert/out/Fcitx.py0000664000175000017500000003173115000453435020465 0ustar00jimmyjimmyfrom ..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././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744983664.0 ssfconv-1.2.2/src/ssfconv/convert/out/Fcitx5.py0000664000175000017500000002367715000453160020557 0ustar00jimmyjimmyfrom ..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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1742720619.0 ssfconv-1.2.2/src/ssfconv/convert/out/__init__.py0000664000175000017500000000013314767747153021165 0ustar00jimmyjimmy__all__ = ["Fcitx", "Fcitx5"] from .Fcitx import ssf2fcitx from .Fcitx5 import ssf2fcitx5 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744983866.5368226 ssfconv-1.2.2/src/ssfconv/extract/0000775000175000017500000000000015000453473016216 5ustar00jimmyjimmy././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1742720619.0 ssfconv-1.2.2/src/ssfconv/extract/__init__.py0000664000175000017500000000000214767747153020343 0ustar00jimmyjimmy ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1742913469.0 ssfconv-1.2.2/src/ssfconv/extract/ssf.py0000664000175000017500000000366014770537675017414 0ustar00jimmyjimmyfrom 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)././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743300678.0 ssfconv-1.2.2/src/ssfconv/translation.py0000664000175000017500000000056414772124106017464 0ustar00jimmyjimmy# 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././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744983866.5368226 ssfconv-1.2.2/src/ssfconv.egg-info/0000775000175000017500000000000015000453473016236 5ustar00jimmyjimmy././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744983866.0 ssfconv-1.2.2/src/ssfconv.egg-info/PKG-INFO0000644000175000017500000001160415000453472017332 0ustar00jimmyjimmyMetadata-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 # 简介 ![gplv3](./gplv3-with-text-136x68.png) 一个将皮肤从搜狗格式转换为 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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744983866.0 ssfconv-1.2.2/src/ssfconv.egg-info/SOURCES.txt0000664000175000017500000000117515000453472020125 0ustar00jimmyjimmyLICENSE 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././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744983866.0 ssfconv-1.2.2/src/ssfconv.egg-info/dependency_links.txt0000664000175000017500000000000115000453472022303 0ustar00jimmyjimmy ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744983866.0 ssfconv-1.2.2/src/ssfconv.egg-info/entry_points.txt0000664000175000017500000000005515000453472021533 0ustar00jimmyjimmy[console_scripts] ssfconv = ssfconv.cli:main ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744983866.0 ssfconv-1.2.2/src/ssfconv.egg-info/requires.txt0000664000175000017500000000003315000453472020631 0ustar00jimmyjimmypycryptodomex pillow numpy ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744983866.0 ssfconv-1.2.2/src/ssfconv.egg-info/top_level.txt0000664000175000017500000000001015000453472020756 0ustar00jimmyjimmyssfconv