├── LICENSE
├── README.md
├── gplv3-with-text-136x68.png
├── pyproject.toml
└── src
└── ssfconv
├── __init__.py
├── cli.py
├── convert
├── CaseSensitiveConfigParser.py
├── __init__.py
├── image_operation.py
├── ini_ssf_operation.py
└── out
│ ├── Fcitx.py
│ ├── Fcitx5.py
│ └── __init__.py
├── extract
├── __init__.py
└── ssf.py
└── translation.py
/LICENSE:
--------------------------------------------------------------------------------
1 | This package is free software; you can redistribute it and/or modify
2 | it under the terms of the GNU General Public License as published by
3 | the Free Software Foundation; either version 3 of the License, or
4 | (at your option) any later version.
5 | .
6 | This package is distributed in the hope that it will be useful,
7 | but WITHOUT ANY WARRANTY; without even the implied warranty of
8 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 | GNU General Public License for more details.
10 | .
11 | You should have received a copy of the GNU General Public License
12 | along with this program. If not, see
13 | .
14 | On Debian systems, the complete text of the GNU General
15 | Public License version 3 can be found in "/usr/share/common-licenses/GPL-3".
16 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 什么?巨硬不给用github了?那我只好基于原则性的考虑,换个地方接着干 https://codeberg.org/RadND/ssfconv
2 |
3 | # 简介
4 |
5 | 
6 |
7 | 一个将皮肤从搜狗格式转换为 fcitx/fcitx5 格式的脚本,如果您在寻找“原生”的fcitx5皮肤,可以看[这里](https://github.com/topics/fcitx5-theme)
8 |
9 | 新开了仓库,因为作者的账号很久没有活动过,我所做的改动也很大:
10 |
11 | 1. 制成 python 包,用户不需要自己装依赖了
12 | 2. 重构,减少重复,将原先的脚本拆分为解包和转换两部分
13 | 3. 注册命令行程序、调整命令行参数、中文本地化
14 | 4. 由于使用了match case语法,python版本要求上升到3.10,如果想用旧版本python运行,请自行把相关代码改回长串 if elif
15 |
16 | 程序的目的没有变,所以原作者的[参考图像](https://www.fkxxyz.com/d/ssfconv)依然适用
17 |
18 | # 安装
19 | ## 手动
20 |
21 | clone或下载release
22 |
23 | ```shell
24 | python -m venv venv_name
25 | source venv_name/bin/activate
26 | # source venv_name/bin/activate.fish
27 | pip install .
28 | ```
29 |
30 | ## pip
31 |
32 | [ ] 下次一定上传到 pypi
33 |
34 | ```shell
35 | pip install ssfconv
36 | ```
37 |
38 | # 使用
39 |
40 | 本包提供的命令行程序加 `-h` 参数就可以查看帮助
41 |
42 | ## 获取皮肤
43 |
44 | 可以从[搜狗的皮肤站](https://pinyin.sogou.com/skins/)或bilibili搜索输入法皮肤获取自己喜欢的皮肤,得到ssf格式的文件,例如 【雨欣】蒲公英的思念.ssf
45 |
46 | ## 解包 ssf 皮肤
47 |
48 | ```shell
49 | ssfconv unpack 【雨欣】蒲公英的思念.ssf
50 | ```
51 |
52 | 得到的文件夹可供 `ssfconv` 使用
53 |
54 | ## 转换为 fcitx4 皮肤
55 |
56 | ```shell
57 | ssfconv convert -t fcitx 【雨欣】蒲公英的思念
58 | ```
59 |
60 | 复制到用户皮肤目录
61 |
62 | ```shell
63 | mkdir -p ~/.config/fcitx/skin/
64 | cp -r 【雨欣】蒲公英的思念 ~/.config/fcitx/skin/
65 | ```
66 |
67 | 右键输入法托盘图表,选中皮肤,这款皮肤是不是出现在列表里了呢,尽情享用吧。
68 |
69 | ## 转换为 fcitx5 主题
70 |
71 | ```shell
72 | ssfconv convert 【雨欣】蒲公英的思念
73 | ```
74 |
75 | 复制到用户主题目录
76 |
77 | ```shell
78 | mkdir -p ~/.local/share/fcitx5/themes/
79 | cp -r 【雨欣】蒲公英的思念 ~/.local/share/fcitx5/themes/
80 | ```
81 |
82 | 打开 fcitx5 的配置,附加组件标签,经典用户界面,点配置,在主题的下拉列表里,选择这款皮肤。
83 |
84 | 或者你也可以直接修改配置文件 ~/.config/fcitx5/conf/classicui.conf,将 Theme 的值改成这个皮肤的名称即可。
85 |
86 | 查看该皮肤在配置文件中的名称:
87 |
88 | ```shell
89 | grep Name ~/.local/share/fcitx5/themes/【雨欣】蒲公英的思念/theme.conf
90 | ```
91 |
92 | # 微调
93 |
94 | 转换得到的皮肤配置或多或少会有点瑕疵,其实调整它们并不困难,快去试试吧
95 |
96 | # 致谢
97 |
98 | 前两位作者的仓库分别在
99 | - https://github.com/fkxxyz/ssfconv
100 | - https://github.com/VOID001/ssf2fcitx
101 |
102 | 前两位作者的账号都不活动,也许已经对系统美化不感兴趣了。不管怎样,我把他们写在了 `pyproject.toml` 中,在此对他们表示感谢。( •ᴗ• )
103 |
104 | 还要感谢 [`那些时光.ssf`](https://pinyin.sogou.com/d/skins/download.php?skin_id=344544) 的作者 szwjerry ,为了用上这个皮肤,我才接触到这个工具。
105 |
106 | # 贡献征集
107 | ## 方格示意
108 |
109 | 需要一个调试工具,能依据转换后的皮肤配置文件在图片上划线,用以表明这个皮肤是怎样被拉伸的
110 |
111 | 考虑到这和转换的过程无关,更像是一个独立的程序
112 |
113 | ## 更多整理
114 |
115 | 转换部分的主函数仍然非常长,需要拆分,有部分固定的配置可以抽离出来以模板文件的形式存在
116 |
117 | ## IBus
118 |
119 | ```
120 | GNOME桌面和非GNOME桌面的IBus采用了两个不同的前端。非GNOME桌面的IBus是项目自己本身基于GTK写的一个很简陋的前端,其使用GTK主题,根据我之前的研究,要实现搜狗那样的皮肤效果应该不太可能。GNOME在他们的GJS代码库里给IBus重写了一个前端,他们的前端可定制性更强,主要是使用CSS文件指定样式
121 | @HollowMan6
122 | ```
123 |
124 | 仅靠转换程序做不到在 IBus 上实现搜狗这种美观的皮肤效果,看样子最有希望的办法是通过 GNOME Shell 扩展修改输入法前端,先行提供背景图片等素材的显示和拉伸/压缩的能力
125 |
126 | readme有点长,其他不重要的内容看 [Wiki 页面](https://github.com/RadND/ssfconv/wiki)
127 |
--------------------------------------------------------------------------------
/gplv3-with-text-136x68.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RadND/ssfconv/9fdbed9cb80907044917e53383216952675f1037/gplv3-with-text-136x68.png
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["setuptools>=61.0"]
3 | build-backend = "setuptools.build_meta"
4 |
5 | [project]
6 | name = "ssfconv"
7 | version = "1.2.1"
8 | dependencies = ["pycryptodomex", "pillow", "numpy"]
9 | requires-python = ">=3.10"
10 | authors = [
11 | { name = "nihui" },
12 | { name = "VOID001" },
13 | { name = "fkxxyz" },
14 | { name = "RadND" },
15 | ]
16 | description = "Sogou input method skin file (.ssf file) converter"
17 | readme = "README.md"
18 | keywords = ["ssf", "fcitx", "fcitx5", "Fcitx5 Classic User interface"]
19 | classifiers = [
20 | "Programming Language :: Python :: 3",
21 | "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
22 | "Operating System :: OS Independent",
23 | "Environment :: Console",
24 | "Intended Audience :: End Users/Desktop",
25 | "Natural Language :: Chinese (Simplified)",
26 | "Topic :: Adaptive Technologies",
27 | ]
28 |
29 | [project.urls]
30 | Repository = "https://github.com/radnd/ssfconv.git"
31 | Issues = "https://github.com/radnd/ssfconv/issues"
32 |
33 | [project.scripts]
34 | ssfconv = "ssfconv.cli:main"
35 |
--------------------------------------------------------------------------------
/src/ssfconv/__init__.py:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/ssfconv/cli.py:
--------------------------------------------------------------------------------
1 | import sys, shutil
2 | from pathlib import Path
3 | from .translation import _
4 | import argparse
5 | from .extract.ssf import extract_ssf
6 | from .convert import convert
7 |
8 |
9 | def add_parser_unpack(subparsers: argparse._SubParsersAction):
10 | parser = subparsers.add_parser("unpack", help=_("Unpack skin"))
11 | parser.add_argument("src", type=Path, help=_("Skin file"))
12 | parser.add_argument(
13 | "dest",
14 | type=Path,
15 | help=_("Output folder name, default is same as Skin file name"),
16 | nargs="?",
17 | default=None,
18 | )
19 | parser.add_argument(
20 | "-t",
21 | "--type",
22 | help=_(
23 | "Format of skin to be unpack",
24 | ),
25 | default="ssf",
26 | choices=["ssf"],
27 | )
28 | parser.add_argument(
29 | "-f",
30 | "--force",
31 | help=_("Delete if output path already exist"),
32 | action="store_true",
33 | )
34 | parser.set_defaults(func=unpack)
35 |
36 |
37 | def unpack(args):
38 | if args.dest == None:
39 | args.dest = args.src.with_suffix("")
40 |
41 | if not args.src.exists():
42 | sys.stderr.write(_("Input path %s not exist\n") % args.src)
43 | return 1
44 | if not args.src.is_file():
45 | sys.stderr.write(_("Input path %s is not file\n") % args.src)
46 | return 1
47 | if args.dest.exists():
48 | if args.force:
49 | if args.dest.is_dir():
50 | shutil.rmtree(args.dest)
51 | else:
52 | args.dest.unlink()
53 | else:
54 | sys.stderr.write(_("Output path %s already exist\n") % args.dest)
55 | return 1
56 | else:
57 | args.dest.mkdir()
58 |
59 | return extract_ssf(args.src, args.dest)
60 |
61 |
62 | def add_parser_convert(subparsers: argparse._SubParsersAction):
63 | parser = subparsers.add_parser("convert", help=_("Convert skin file"))
64 | parser.add_argument("src", type=Path, help=_("Input folder"))
65 | # parser.add_argument(
66 | # "dest", help="输出文件夹,默认与输入文件夹相同", nargs="?", default=None
67 | # )
68 | parser.add_argument(
69 | "-t",
70 | "--type",
71 | help=_("Output skin type"),
72 | default="fcitx5",
73 | choices=["fcitx", "fcitx5"],
74 | )
75 | # parser.add_argument(
76 | # "-f", "--force", help="强制覆盖输出文件夹的内容", action="store_true"
77 | # )
78 | parser.add_argument(
79 | "-i",
80 | "--install",
81 | help=_("Install the convert result to it's default install location"),
82 | action="store_true",
83 | )
84 | parser.set_defaults(func=conv)
85 |
86 |
87 | def conv(args):
88 | if not args.src.exists():
89 | sys.stderr.write(_("Input path %s not exist\n") % args.src)
90 | return 1
91 | if not args.src.is_dir():
92 | sys.stderr.write(_("Input path %s is not folder\n") % args.src)
93 | return 1
94 |
95 | args.dest = args.src
96 | if args.dest.exists():
97 | # if not args.force:
98 | # sys.stderr.write("输出文件(夹) %s 已存在\n" % args.dest)
99 | # return 1
100 | pass
101 | else:
102 | args.dest.mkdir()
103 |
104 | err = convert(args)
105 | exit(err)
106 |
107 |
108 | def main():
109 | parser = argparse.ArgumentParser(prog="ssfconv")
110 | # parser.add_argument("--foo", action="store_true", help="foo help")
111 | subparsers = parser.add_subparsers()
112 |
113 | add_parser_unpack(subparsers)
114 | add_parser_convert(subparsers)
115 |
116 | # print(parser.parse_args(["a", "12"]))
117 | args = parser.parse_args()
118 | args.func(args)
119 |
--------------------------------------------------------------------------------
/src/ssfconv/convert/CaseSensitiveConfigParser.py:
--------------------------------------------------------------------------------
1 | import configparser
2 |
3 | # 为了使其区分大小写,重载 ConfigParser
4 | class CaseSensitiveConfigParser(configparser.ConfigParser):
5 | def optionxform(self, optionstr):
6 | return optionstr
--------------------------------------------------------------------------------
/src/ssfconv/convert/__init__.py:
--------------------------------------------------------------------------------
1 | from .out import ssf2fcitx, ssf2fcitx5
2 | from pathlib import Path
3 | import os, sys, shutil
4 | import logging
5 |
6 |
7 | def convert(args):
8 | install_dir = None
9 | # TODO refractor ssf2fcitx{5} get rid of os.path
10 | match args.type:
11 | case "fcitx5":
12 | err = ssf2fcitx5(args.src)
13 | case "fcitx":
14 | err = ssf2fcitx(args.src)
15 | case _:
16 | assert False
17 | if args.install:
18 | install(args)
19 | return err
20 |
21 |
22 | default_skins_dir = {
23 | "fcitx5": "fcitx5/themes/",
24 | "fcitx": "fcitx/skin/",
25 | }
26 |
27 |
28 | def install(args):
29 | match args.type:
30 | case "fcitx5":
31 | install_dir = os.getenv("XDG_DATA_HOME", Path("~/.local/share").expanduser())
32 | case "fcitx":
33 | install_dir = os.getenv("XDG_CONFIG_HOME", Path("~/.config").expanduser())
34 | case _:
35 | assert False
36 | install_dir = Path(install_dir) / default_skins_dir[args.type]
37 | shutil.move(args.dest, install_dir)
38 |
--------------------------------------------------------------------------------
/src/ssfconv/convert/image_operation.py:
--------------------------------------------------------------------------------
1 | from PIL import Image, ImageDraw
2 | import numpy as np
3 | import logging
4 | from pathlib import Path
5 |
6 |
7 | def getImageAvg(image_path:Path, area=(0, 0, 0, 0)):
8 | """
9 | 获取图片的像素平均值
10 | image_path 图片的路径
11 | aria 是需要求的平均值的区域,默认整幅图
12 | 格式 area = (x1,x2,y1,y2)
13 | 当 x2 或 y2 为零表示最大值直到边界
14 | 为负时表示距离最大边界多少的坐标
15 | 返回 (r,g,b) 三元组
16 | """
17 |
18 | with Image.open(image_path) as file:
19 | size = file.size
20 |
21 | # 确定区域
22 | x1 = area[0] % size[0]
23 | x2 = area[1] % size[0]
24 | y1 = area[2] % size[1]
25 | y2 = area[3] % size[1]
26 | if x2 == 0:
27 | x2 = size[0]
28 | if y2 == 0:
29 | y2 = size[1]
30 |
31 | if x1 > x2:
32 | t = x1
33 | x1 = x2
34 | x2 = t
35 | if y1 > y2:
36 | t = y1
37 | y1 = y2
38 | y2 = t
39 | if x1 == x2:
40 | if x2 != size[0]:
41 | x2 += 1
42 | else:
43 | x1 -= 1
44 | if y1 == y2:
45 | if y2 != size[1]:
46 | y2 += 1
47 | else:
48 | y1 -= 1
49 |
50 | # 算出区域内所有像素点的平均值
51 | img = np.asarray(file)
52 | r = g = b = 0
53 | count = 0
54 |
55 | # 有没有透明度?
56 | if len(img.shape) < 3:
57 | pass
58 | elif img.shape[2] == 4:
59 | for y in range(y1, y2):
60 | for x in range(x1, x2):
61 | if img[y][x][3] > 0:
62 | r += img[y][x][0]
63 | g += img[y][x][1]
64 | b += img[y][x][2]
65 | count += 1
66 | else:
67 | for y in range(y1, y2):
68 | for x in range(x1, x2):
69 | r += img[y][x][0]
70 | g += img[y][x][1]
71 | b += img[y][x][2]
72 | count += 1
73 |
74 | if count == 0:
75 | count = 1
76 | # https://github.com/fkxxyz/ssfconv/issues/20
77 | r = int(r)
78 | g = int(g)
79 | b = int(b)
80 |
81 | r //= count
82 | g //= count
83 | b //= count
84 | return (r, g, b)
85 |
86 |
87 | # 获取图片大小的函数
88 | def getImageSize(image_file:str):
89 | with Image.open(image_file) as file:
90 | size = file.size
91 | assert size[0] > 0 and size[0] < 65536 and size[1] > 0 and size[1] < 65536
92 | return size
93 |
94 |
95 | # 保存一个多边形到文件
96 | def savePolygon(size, points, color, out_file:str):
97 | img = Image.new("RGBA", size)
98 | draw = ImageDraw.Draw(img)
99 | draw.polygon(points, fill=color)
100 | img.save(out_file)
101 |
102 |
103 | def rgbDistSqure(c1, c2):
104 | """
105 | 简单的计算两个颜色之间的距离
106 | """
107 | dr = c1[0] - c2[0]
108 | dg = c1[1] - c2[1]
109 | db = c1[2] - c2[2]
110 | return dr * dr + dg * dg + db * db
111 |
112 |
113 | def rgbDistMax(color, *colors):
114 | """
115 | 求 colors 中与 color 的距离最大的颜色
116 | """
117 | max_dist = 0
118 | max_dist_color = colors[0]
119 | for c in colors:
120 | # basic-colormath 的距离计算方法更精确,但这一切值得吗
121 | cur_dist = rgbDistSqure(color, c)
122 | if max_dist < cur_dist:
123 | max_dist = cur_dist
124 | max_dist_color = c
125 | return max_dist_color
126 |
--------------------------------------------------------------------------------
/src/ssfconv/convert/ini_ssf_operation.py:
--------------------------------------------------------------------------------
1 | from .CaseSensitiveConfigParser import CaseSensitiveConfigParser
2 | from .image_operation import *
3 | import io
4 | from pathlib import Path
5 | import sys
6 | from ssfconv.translation import _
7 |
8 | default_menu_img_bin = io.BytesIO(
9 | 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"
10 | )
11 | default_radio_img_bin = io.BytesIO(
12 | 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'
13 | )
14 |
15 |
16 | class SsfIniWrapper:
17 | def __init__(self, skin_dir: Path):
18 | self.ssf = None
19 | self.skin_dir: Path = skin_dir
20 | self.read_ssf_ini()
21 |
22 | def read_ssf_ini(self):
23 | """
24 | 为了重复使用,不和init合并
25 | """
26 | skin_ini = self.skin_dir / "skin.ini"
27 | if not skin_ini.is_file():
28 | sys.stderr.write(_("Cant found skin.ini\n"))
29 | return 1
30 |
31 | try:
32 | ssf_ini = CaseSensitiveConfigParser(allow_no_value=True)
33 | ssf_ini.read(skin_ini, encoding="utf-16")
34 | except:
35 | sys.stderr.write(_("Failed to read skin.ini\n"))
36 | return 2
37 | self.ssf = ssf_ini
38 | return
39 |
40 | def get_image_config(self, section, key, index=0):
41 | """
42 | 获取图片文件名的函数(获取失败则返回空字符串)
43 | """
44 | image_name_list = self.ssf[section].get(key, "").split(",")
45 | if index < len(image_name_list):
46 | image_name = image_name_list[index]
47 | if (self.skin_dir / image_name).is_file():
48 | return image_name
49 | else:
50 | return ""
51 |
52 | def try_get_value(self, section, key):
53 | return self.ssf[section].get(key, "").strip()
54 |
55 | def getRawIni(self):
56 | # FIXME 允许写这个变量是坏主意,考虑使用私有变量或私有方法来暗示
57 | return self.ssf
58 |
59 | def findBackgroundColorBy(self, key):
60 | # 排除键值不存在
61 | image_name = self.get_image_config(key[0], key[1])
62 | if not image_name:
63 | return None
64 |
65 | # 排除区域不存在
66 | h_str = self.try_get_value(key[0], key[1][:-3] + "layout_horizontal")
67 | if not h_str:
68 | return None
69 | v_str = self.try_get_value(key[0], key[1][:-3] + "layout_vertical")
70 | if not v_str:
71 | return None
72 |
73 | # 得出区域
74 | h = h_str.split(",")
75 | v = v_str.split(",")
76 | if len(h) != 3 or len(v) != 3:
77 | return None
78 |
79 | # 排除平铺模式(筛选出是拉伸区域)
80 | # if int(h[0]) != 0 or int(v[0]) != 0:
81 | # return None
82 |
83 | return getImageAvg(
84 | self.skin_dir / image_name,
85 | (int(h[1]), -int(h[2]), int(v[1]), -int(v[2])),
86 | )
87 |
88 | def findBackgroundColor(self):
89 | """
90 | 根据里面所有的图片,根据所设置的拉伸区域确定合适的背景色
91 | 不知道原作者为什么按顺序取到任意一张就完成
92 | """
93 | keys = (
94 | ("Scheme_V1", "pic"),
95 | ("Scheme_V2", "pinyin_pic"),
96 | ("Scheme_V2", "zhongwen_pic"),
97 | ("Scheme_H1", "pic"),
98 | ("Scheme_H2", "pinyin_pic"),
99 | ("Scheme_H2", "zhongwen_pic"),
100 | )
101 | for key in keys:
102 | avg_color = self.findBackgroundColorBy(key)
103 | if avg_color:
104 | return avg_color
105 | else:
106 | return (0, 0, 0)
107 |
108 |
109 | # 将 skin.ini 的颜色转换成 (r,g,b) 三元组
110 | def colorConv(ssf_color):
111 | color_int = int(ssf_color, 16)
112 | r = color_int % 256
113 | g = (color_int % 65536) // 256
114 | b = color_int // 65536
115 | return (r, g, b)
116 |
--------------------------------------------------------------------------------
/src/ssfconv/convert/out/Fcitx.py:
--------------------------------------------------------------------------------
1 | from ..image_operation import *
2 | from ..ini_ssf_operation import *
3 |
4 | #TODO 用pathlib 替换
5 | import os
6 |
7 | # 创建符号链接的函数(若存在则覆盖)
8 | def symlinkF(src, dst):
9 | if os.path.isfile(dst):
10 | os.remove(dst)
11 | return os.symlink(src, dst)
12 |
13 | def makeConfFromSsf(ssf):
14 | skin = CaseSensitiveConfigParser(allow_no_value = True)
15 |
16 | skin['SkinInfo'] = {
17 | # 皮肤名称
18 | 'Name': ssf['General']['skin_name'],
19 |
20 | # 皮肤版本
21 | 'Version': ssf['General']['skin_version'],
22 |
23 | # 皮肤作者
24 | 'Author': ssf['General']['skin_author'],
25 |
26 | # 描述
27 | 'Desc': ssf['General']['skin_info'],
28 | }
29 | return skin
30 |
31 |
32 | def ssf2fcitx(skin_dir:str):
33 | """
34 | 转换为 fcitx 格式
35 | 将解压后的 ssf 皮肤,在里面创建出 fcitx_skin.conf
36 | """
37 |
38 | ssfw = SsfIniWrapper(Path(skin_dir))
39 | ssf=ssfw.getRawIni()
40 |
41 | skin=makeConfFromSsf(ssf)
42 |
43 | # 输入框输入的拼音颜色
44 | input_color = colorConv(ssf['Display']['pinyin_color'])
45 |
46 | # 列表中第一个词的颜色
47 | first_color = colorConv(ssf['Display']['zhongwen_first_color'])
48 |
49 | # 列表中其他词的颜色
50 | other_color = colorConv(ssf['Display']['zhongwen_color'])
51 |
52 | # 根据里面所有的图片,根据所设置的拉伸区域确定合适的背景色
53 | back_color = ssfw.findBackgroundColor()
54 |
55 | # 字体大小(像素)
56 | font_size = int(ssf['Display']['font_size'])
57 |
58 | # 状态栏背景图
59 | static_bar_image = ssfw.get_image_config('StatusBar', 'pic')
60 |
61 | # 中/英文状态
62 | cn_status_image = ssfw.get_image_config('StatusBar', 'cn_en', 0)
63 | en_status_image = ssfw.get_image_config('StatusBar', 'cn_en', 1)
64 |
65 | # 全半角状态
66 | quan_status_image = ssfw.get_image_config('StatusBar', 'quan_ban', 0)
67 | ban_status_image = ssfw.get_image_config('StatusBar', 'quan_ban', 1)
68 |
69 | # 中/英文标点状态
70 | cn_p_status_image = ssfw.get_image_config('StatusBar', 'biaodian', 0)
71 | en_p_status_image = ssfw.get_image_config('StatusBar', 'biaodian', 1)
72 |
73 | # 繁/简状态
74 | simp_status_image = ssfw.get_image_config('StatusBar', 'fan_jian', 1)
75 | trad_status_image = ssfw.get_image_config('StatusBar', 'fan_jian', 0)
76 |
77 | # 虚拟键盘状态
78 | vk_inactive_status_image = ssfw.get_image_config('StatusBar', 'softkeyboard')
79 | for mouse_status in ('down','in','out','downing'):
80 | vk_active_status_image = ssfw.get_image_config('StatusBar', 'softkeyboard_' + mouse_status)
81 | if vk_active_status_image:
82 | break
83 |
84 | icons = (cn_status_image, simp_status_image, trad_status_image,
85 | quan_status_image, ban_status_image,
86 | cn_p_status_image, en_p_status_image,
87 | vk_inactive_status_image, vk_active_status_image)
88 |
89 | # 求图标的前景色(任意一个即可)
90 | for image in icons:
91 | if image:
92 | icon_color = getImageAvg(Path(skin_dir) / image)
93 | break
94 | else:
95 | icon_color = other_color
96 |
97 | skin['SkinFont'] = {
98 | # 字体大小
99 | 'FontSize': font_size,
100 |
101 | # 菜单字体大小
102 | 'MenuFontSize': 14,
103 |
104 | # 字体大小遵守dpi设置
105 | 'RespectDPI': 'False',
106 |
107 | # 提示信息颜色
108 | 'TipColor': '%d %d %d' % first_color,
109 |
110 | # 输入信息颜色
111 | 'InputColor': '%d %d %d' % other_color,
112 |
113 | # 候选词索引颜色
114 | 'IndexColor': '%d %d %d' % other_color,
115 |
116 | # 第一候选词颜色
117 | 'FirstCandColor': '%d %d %d' % first_color,
118 |
119 | # 用户词组颜色
120 | 'UserPhraseColor': '%d %d %d' % first_color,
121 |
122 | # 码表提示颜色
123 | 'CodeColor': '%d %d %d' % input_color,
124 |
125 | # 其他颜色
126 | 'OtherColor': '%d %d %d' % other_color,
127 |
128 | # 活动菜单项颜色
129 | 'ActiveMenuColor': '%d %d %d' % \
130 | rgbDistMax(other_color,
131 | first_color, input_color, back_color, icon_color),
132 |
133 | # 非活动菜单项颜色+状态栏图标文字颜色
134 | 'InactiveMenuColor': '%d %d %d' % \
135 | rgbDistMax(back_color,
136 | first_color, input_color, other_color, icon_color),
137 | }
138 |
139 | # 创建中文拼音状态图 pinyin.png
140 | if cn_status_image:
141 | symlinkF(cn_status_image, skin_dir + os.sep + 'pinyin.png')
142 |
143 | # 创建全/半角状态图 fullwidth_active.png / fullwidth_inactive.png
144 | if quan_status_image:
145 | symlinkF(quan_status_image, skin_dir + os.sep + 'fullwidth_active.png')
146 | if ban_status_image:
147 | symlinkF(ban_status_image, skin_dir + os.sep + 'fullwidth_inactive.png')
148 |
149 | # 创建中/英文标点状态图 punc_active.png / punc_inactive.png
150 | if cn_p_status_image:
151 | symlinkF(cn_p_status_image, skin_dir + os.sep + 'punc_active.png')
152 | if en_p_status_image:
153 | symlinkF(en_p_status_image, skin_dir + os.sep + 'punc_inactive.png')
154 |
155 | # 创建繁/简状态图 chttrans_inactive.png / chttrans_active.png
156 | if simp_status_image:
157 | symlinkF(simp_status_image, skin_dir + os.sep + 'chttrans_inactive.png')
158 | if trad_status_image:
159 | symlinkF(trad_status_image, skin_dir + os.sep + 'chttrans_active.png')
160 |
161 | # 创建虚拟键盘状态图 vk_inactive.png / vk_active.png
162 | if vk_inactive_status_image:
163 | symlinkF(vk_inactive_status_image, skin_dir + os.sep + 'vk_inactive.png')
164 | if vk_active_status_image:
165 | symlinkF(vk_active_status_image, skin_dir + os.sep + 'vk_active.png')
166 |
167 | # 求搜狗状态栏上几个按钮的坐标的最值
168 | x_min = y_min = 65536
169 | x_max = y_max = 0
170 | for button in ('cn_en',
171 | 'biaodian',
172 | 'quan_ban',
173 | 'quan_shuang',
174 | 'fan_jian',
175 | 'softkeyboard',
176 | 'menu',
177 | 'sogousearch',
178 | 'passport',
179 | 'skinmanager'):
180 | display = ssfw.try_get_value('StatusBar', button + '_display')
181 | if display != '1': continue
182 | pos = ssfw.try_get_value('StatusBar', button + '_pos').split(',')
183 | if len(pos) != 2: continue
184 |
185 | # 取最值
186 | if int(pos[0]) < x_min: x_min = int(pos[0])
187 | if int(pos[1]) < y_min: y_min = int(pos[1])
188 |
189 | # 得到图标尺寸
190 | icon_image = ssfw.get_image_config('StatusBar', button, 0)
191 | if not icon_image: continue
192 | size = getImageSize(skin_dir + os.sep + icon_image)
193 |
194 | # 取最右值
195 | x = int(pos[0]) + size[0]
196 | if x > x_max: x_max = x
197 |
198 | y = int(pos[1]) + size[1]
199 | if y > y_max: y_max = y
200 |
201 | # 得出合适的右边距和下边距
202 | if static_bar_image:
203 | size = getImageSize(skin_dir + os.sep + static_bar_image)
204 | MarginRight = size[0] - x_max + 4
205 | MarginBottom = size[1] - y_max + 4
206 | else:
207 | MarginRight = 4
208 | MarginBottom = 4
209 |
210 | skin['SkinMainBar'] = {
211 | # 背景图片
212 | 'BackImg': static_bar_image,
213 |
214 | # Logo图标
215 | 'Logo': '',
216 |
217 | # 英文模式图标
218 | 'Eng': en_status_image,
219 |
220 | # 激活状态输入法图标
221 | 'Active': cn_status_image,
222 |
223 | # 左边距
224 | 'MarginLeft': x_min+4,
225 |
226 | # 右边距
227 | 'MarginRight': MarginRight,
228 |
229 | # 上边距
230 | 'MarginTop': y_min+4,
231 |
232 | # 下边距
233 | 'MarginBottom': MarginBottom,
234 |
235 | # 可点击区域的左边距
236 | #ClickMarginLeft=0
237 | # 可点击区域的右边距
238 | #ClickMarginRight=0
239 | # 可点击区域的上边距
240 | #ClickMarginTop=0
241 | # 可点击区域的下边距
242 | #ClickMarginBottom=0
243 | # 覆盖图片
244 | #Overlay=
245 | # 覆盖图片停靠位置
246 | # Available Value:
247 | # TopLeft
248 | # TopCenter
249 | # TopRight
250 | # CenterLeft
251 | # Center
252 | # CenterRight
253 | # BottomLeft
254 | # BottomCenter
255 | # BottomRight
256 | #OverlayDock=TopLeft
257 | # 覆盖图片 X 偏移
258 | #OverlayOffsetX=0
259 | # 覆盖图片 Y 偏移
260 | #OverlayOffsetY=0
261 | # 纵向填充规则
262 | # Available Value:
263 | # Copy
264 | # Resize
265 | #FillVertical=Resize
266 | # 横向填充规则
267 | # Available Value:
268 | # Copy
269 | # Resize
270 | #FillHorizontal=Resize
271 | # 使用自定的文本图标颜色
272 | # Available Value:
273 | # True False
274 | #UseCustomTextIconColor=True
275 | # 活动的文本图标颜色
276 | #ActiveTextIconColor=101 153 209
277 | # 非活动的文本图标颜色
278 | #InactiveTextIconColor=101 153 209
279 | # 特殊图标位置
280 | #Placement=
281 | }
282 |
283 |
284 | # 输入框背景图
285 | input_bar_image = ssfw.get_image_config('Scheme_H1', 'pic')
286 | input_bar_image_size = getImageSize(skin_dir + os.sep + input_bar_image)
287 |
288 | # 绘制 prev.png 和 next.png 颜色为 '%d %d %d' % other_color
289 | savePolygon((6,12), ((0,0),(6,6),(0,12)), other_color, skin_dir + os.sep + 'next.png')
290 | savePolygon((6,12), ((0,6),(6,0),(6,12)), other_color, skin_dir + os.sep + 'prev.png')
291 |
292 | # 水平边距
293 | lh = ssfw.try_get_value('Scheme_H1', 'layout_horizontal')
294 | if lh:
295 | lh = tuple(map(lambda s:int(s), ssf['Scheme_H1']['layout_horizontal'].split(',')))
296 | else:
297 | lh = (0, 0, 0)
298 |
299 | # 竖直边距
300 | pinyin_marge = ssfw.try_get_value('Scheme_H1', 'pinyin_marge')
301 | if pinyin_marge:
302 | pinyin_marge = tuple(map(lambda s:int(s), pinyin_marge.split(',')))
303 | else:
304 | assert False
305 | zhongwen_marge = ssfw.try_get_value('Scheme_H1', 'zhongwen_marge')
306 | if zhongwen_marge:
307 | zhongwen_marge = tuple(map(lambda s:int(s), zhongwen_marge.split(',')))
308 | else:
309 | assert False
310 | separator = ssfw.try_get_value('Scheme_H1', 'separator')
311 | sep = 1 if separator else 0
312 | InputPos = pinyin_marge[0] + font_size
313 | OutputPos = pinyin_marge[0] + pinyin_marge[1] + font_size + \
314 | sep + zhongwen_marge[0] + font_size
315 | MarginBottom = input_bar_image_size[1] - OutputPos
316 | if lh[1] - pinyin_marge[2] > 32:
317 | MarginLeft = pinyin_marge[2]
318 | else:
319 | MarginLeft = lh[1]
320 |
321 | skin['SkinInputBar'] = {
322 | # 背景图片
323 | 'BackImg': input_bar_image,
324 |
325 | # 左边距
326 | 'MarginLeft': MarginLeft,
327 |
328 | # 右边距
329 | 'MarginRight': lh[2],
330 |
331 | # 上边距
332 | 'MarginTop': 0,
333 |
334 | # 下边距
335 | 'MarginBottom': MarginBottom,
336 |
337 | # 可点击区域的左边距
338 | #ClickMarginLeft=0
339 | # 可点击区域的右边距
340 | #ClickMarginRight=0
341 | # 可点击区域的上边距
342 | #ClickMarginTop=0
343 | # 可点击区域的下边距
344 | #ClickMarginBottom=0
345 | # 覆盖图片
346 | #Overlay=hangul.png
347 | # 覆盖图片停靠位置
348 | # Available Value:
349 | # TopLeft
350 | # TopCenter
351 | # TopRight
352 | # CenterLeft
353 | # Center
354 | # CenterRight
355 | # BottomLeft
356 | # BottomCenter
357 | # BottomRight
358 | #OverlayDock=TopRight
359 | # 覆盖图片 X 偏移
360 | #OverlayOffsetX=-26
361 | # 覆盖图片 Y 偏移
362 | #OverlayOffsetY=2
363 |
364 | # 光标颜色
365 | 'CursorColor': '%d %d %d' % first_color,
366 |
367 | # 预编辑文本的位置或偏移
368 | 'InputPos': InputPos,
369 |
370 | # 候选词表的位置或偏移
371 | 'OutputPos': OutputPos,
372 |
373 | # 上一页图标
374 | 'BackArrow': 'prev.png',
375 |
376 | # 下一页图标
377 | 'ForwardArrow': 'next.png',
378 |
379 | # 上一页图标的横坐标
380 | 'BackArrowX': lh[2] - lh[1] + 10,
381 |
382 | # 上一页图标的纵坐标
383 | 'BackArrowY': pinyin_marge[0],
384 |
385 | # 下一页图标的横坐标
386 | 'ForwardArrowX': lh[2] - lh[1],
387 |
388 | # 下一页图标的纵坐标
389 | 'ForwardArrowY': pinyin_marge[0],
390 |
391 | # 纵向填充规则
392 | # Available Value:
393 | # Copy
394 | # Resize
395 | #FillVertical=Resize
396 | # 横向填充规则
397 | # Available Value:
398 | # Copy
399 | # Resize
400 | #FillHorizontal=Resize
401 | }
402 |
403 | # 使用系统默认的 active.png 和 inactive.png
404 | symlinkF('/usr/share/fcitx/skin/default/active.png',
405 | skin_dir + os.sep + 'active.png')
406 | symlinkF('/usr/share/fcitx/skin/default/inactive.png',
407 | skin_dir + os.sep + 'inactive.png')
408 |
409 | skin['SkinTrayIcon'] = {
410 | # 活动输入法图标
411 | 'Active': 'active.png',
412 |
413 | # 非活动输入法图标
414 | 'Inactive': 'inactive.png',
415 | }
416 |
417 | # 用纯背景色构建出本主题的 menu.png
418 | img = Image.open(default_menu_img_bin)
419 | a = np.array(img)
420 | for i in range(len(a)):
421 | for j in range(len(a[0])):
422 | if a[i][j][3]:
423 | a[i][j][0] = back_color[0]
424 | a[i][j][1] = back_color[1]
425 | a[i][j][2] = back_color[2]
426 | img = Image.fromarray(a)
427 | img.save(skin_dir + os.sep + 'menu.png')
428 |
429 | skin['SkinMenu'] = {
430 | # 背景图片
431 | 'BackImg': 'menu.png',
432 |
433 | # 上边距
434 | 'MarginTop': 8,
435 |
436 | # 下边距
437 | 'MarginBottom': 8,
438 |
439 | # 左边距
440 | 'MarginLeft': 8,
441 |
442 | # 右边距
443 | 'MarginRight': 8,
444 |
445 | # 活动菜单项颜色
446 | 'ActiveColor': '%d %d %d' % other_color,
447 |
448 | # 分隔线颜色
449 | 'LineColor': '%d %d %d' % other_color,
450 | }
451 |
452 | skin['SkinKeyboard'] = {
453 | # 虚拟键盘图片
454 | #BackImg=keyboard.png
455 |
456 | # 软键盘按键文字颜色
457 | #'KeyColor': '%d %d %d' % first_color,
458 | }
459 |
460 | skin.write(open(skin_dir + os.sep + 'fcitx_skin.conf', 'w', encoding="utf-8"), False)
461 | return 0
--------------------------------------------------------------------------------
/src/ssfconv/convert/out/Fcitx5.py:
--------------------------------------------------------------------------------
1 | from ..ini_ssf_operation import *
2 |
3 | #TODO 用pathlib 替换
4 | import os
5 |
6 | def makeConfFromSsf(ssf):
7 | skin = CaseSensitiveConfigParser(allow_no_value = True)
8 |
9 | skin['Metadata'] = {
10 | # 皮肤名称
11 | 'Name': ssf['General']['skin_name'],
12 |
13 | # 皮肤版本
14 | 'Version': ssf['General']['skin_version'],
15 |
16 | # 皮肤作者
17 | 'Author': ssf['General']['skin_author'],
18 |
19 | # 描述
20 | 'Description': ssf['General']['skin_info'],
21 |
22 | # 用 DPI 缩放
23 | 'ScaleWithDPI': 'False',
24 | }
25 | return skin
26 |
27 |
28 | def ssf2fcitx5(skin_dir:str):
29 | """
30 | 转换为 fcitx5 格式
31 | 将解压后的 ssf 皮肤,在里面创建出 theme.conf
32 | """
33 |
34 | ssfw = SsfIniWrapper(Path(skin_dir))
35 | ssf=ssfw.getRawIni()
36 |
37 | skin=makeConfFromSsf(ssf)
38 |
39 | # 输入框(pre_edit)输入的拼音颜色
40 | input_color = colorConv(ssf['Display']['pinyin_color'])
41 |
42 | # 列表中第一个词的颜色
43 | first_color = colorConv(ssf['Display']['zhongwen_first_color'])
44 |
45 | # 列表中其他词的颜色
46 | other_color = colorConv(ssf['Display']['zhongwen_color'])
47 |
48 | # 根据里面所有的图片,根据所设置的拉伸区域确定合适的背景色
49 | back_color = ssfw.findBackgroundColor()
50 |
51 | # 字体大小(像素)
52 | font_size = int(ssf['Display']['font_size'])
53 |
54 | skin['InputPanel'] = {
55 | # 字体及其大小
56 | 'Font': 'Sans %d' % font_size,
57 |
58 | # 非选中候选字颜色
59 | 'NormalColor': '#%02x%02x%02x' % other_color,
60 |
61 | # 选中候选字颜色
62 | 'HighlightCandidateColor': '#%02x%02x%02x' % first_color,
63 |
64 | # 高亮前景颜色(back_color输入字符颜色)
65 | 'HighlightColor': '#%02x%02x%02x' % input_color,
66 |
67 | # 输入字符背景颜色
68 | 'HighlightBackgroundColor': '#%02x%02x%02x' % back_color,
69 |
70 | #
71 | 'Spacing': 3,
72 | }
73 |
74 | # 输入框背景图
75 | input_bar_image = ssfw.get_image_config('Scheme_H1', 'pic')
76 | input_bar_image_size = getImageSize(skin_dir + os.sep + input_bar_image)
77 |
78 | # 水平拉升区域
79 | lh = ssfw.try_get_value('Scheme_H1', 'layout_horizontal')
80 | if lh:
81 | lh = tuple(map(lambda s:int(s), lh.split(',')))
82 | else:
83 | lh = (0, 2, 2)
84 |
85 | # 垂直拉升区域
86 | lv = ssfw.try_get_value('Scheme_H1', 'layout_vertical')
87 | if lv:
88 | lv = tuple(map(lambda s:int(s), lv.split(',')))
89 | else:
90 | lv = (0, 2, 2)
91 |
92 | # 拼音边距
93 | pinyin_marge = ssfw.try_get_value('Scheme_H1', 'pinyin_marge')
94 | if pinyin_marge:
95 | pinyin_marge = tuple(map(lambda s:int(s), pinyin_marge.split(',')))
96 | else:
97 | assert False
98 |
99 | # 候选词边距
100 | zhongwen_marge = ssfw.try_get_value('Scheme_H1', 'zhongwen_marge')
101 | if zhongwen_marge:
102 | zhongwen_marge = tuple(map(lambda s:int(s), zhongwen_marge.split(',')))
103 | else:
104 | assert False
105 |
106 | # 分隔符长度
107 | sep = 1 if ssfw.try_get_value('Scheme_H1', 'separator') else 0
108 |
109 | # 恒等式:
110 | # 输入的拼音下方到候选词上方的距离:
111 | # pinyin_marge[1] + sep + zhongwen_marge[0] = TextMargin.Bottom + TextMargin.Top
112 | # 输入的拼音上方到上方边界的距离:
113 | # pinyin_marge[0] = ContentMargin.Top + TextMargin.Top
114 | # 候选词下方到下方边界的距离:
115 | # zhongwen_marge[1] = ContentMargin.Bottom + TextMargin.Bottom
116 | #
117 | #
118 | # 这是四元一次方程组,由于只有三个方程,那么随便确定其中一个即可解得其它未知数。
119 | # 增加的方程:
120 | # TextMargin.Bottom = (pinyin_marge[1] + sep + zhongwen_marge[0]) // 2
121 |
122 | distant_pinyin_zhongwen = pinyin_marge[1] + sep + zhongwen_marge[0]
123 |
124 | # 解得:
125 | TextMargin_Bottom = distant_pinyin_zhongwen // 2
126 | TextMargin_Top = distant_pinyin_zhongwen - TextMargin_Bottom
127 | ContentMargin_Top = pinyin_marge[0] - TextMargin_Top
128 | #ContentMargin_Bottom = zhongwen_marge[1] - TextMargin_Bottom
129 | ContentMargin_Bottom = input_bar_image_size[1] - \
130 | ContentMargin_Top - TextMargin_Top - font_size - TextMargin_Bottom - \
131 | TextMargin_Top - font_size - TextMargin_Bottom
132 |
133 | TextMargin_Top_Left = 5
134 | TextMargin_Top_Right = 5
135 |
136 | # 文字边距
137 | skin['InputPanel/TextMargin'] = {
138 | 'Left': TextMargin_Top_Left,
139 | 'Right': TextMargin_Top_Right,
140 | 'Top': TextMargin_Top,
141 | 'Bottom': TextMargin_Bottom,
142 | }
143 |
144 | # 输入框内容边距
145 | skin['InputPanel/ContentMargin'] = {
146 | 'Left': max(pinyin_marge[2], zhongwen_marge[2]) - TextMargin_Top_Left,
147 | 'Right': max(pinyin_marge[3], zhongwen_marge[3]) - TextMargin_Top_Right,
148 | 'Top': ContentMargin_Top,
149 | 'Bottom': ContentMargin_Bottom,
150 | }
151 |
152 | # 输入框背景图
153 | skin['InputPanel/Background'] = {
154 | 'Image': input_bar_image,
155 | }
156 |
157 | # 输入框背景图的拉升区域
158 | skin['InputPanel/Background/Margin'] = {
159 | 'Left': lh[1],
160 | 'Right': lh[2],
161 | 'Top': lv[1],
162 | 'Bottom': lv[2],
163 | }
164 |
165 |
166 | # 绘制高亮的纯色图片
167 | # menu_highlight_color = rgbDistMax(first_color, input_color, other_color, back_color)
168 | Image.new('RGBA', (38,23), (0,0,0,0)).save(skin_dir + os.sep + 'highlight.png')
169 |
170 | # 高亮背景
171 | skin['InputPanel/Highlight'] = {
172 | 'Image': 'highlight.png',
173 | }
174 | # 高亮背景边距
175 | skin['InputPanel/Highlight/Margin'] = {
176 | 'Left': 5,
177 | 'Right': 5,
178 | 'Top': 5,
179 | 'Bottom': 5,
180 | }
181 |
182 |
183 | # 绘制 prev.png 和 next.png 颜色为 '%d %d %d' % other_color
184 | savePolygon((16,24), ((5,6),(5,18),(11,12)), other_color, skin_dir + os.sep + 'next.png')
185 | savePolygon((16,24), ((11,6),(11,18),(5,12)), other_color, skin_dir + os.sep + 'prev.png')
186 |
187 |
188 | # 前一页的箭头
189 | skin['InputPanel/PrevPage'] = {
190 | 'Image': 'prev.png',
191 | }
192 | skin['InputPanel/PrevPage/ClickMargin'] = {
193 | 'Left': 5,
194 | 'Right': 5,
195 | 'Top': 4,
196 | 'Bottom': 4,
197 | }
198 | # 后一页的箭头
199 | skin['InputPanel/NextPage'] = {
200 | 'Image': 'next.png',
201 | }
202 | skin['InputPanel/NextPage/ClickMargin'] = {
203 | 'Left': 5,
204 | 'Right': 5,
205 | 'Top': 4,
206 | 'Bottom': 4,
207 | }
208 |
209 | # 竖排合窗口设置
210 | Scheme_V1_pic = ssfw.try_get_value('Scheme_V1', 'pic')
211 |
212 | # 水平拉升区域
213 | lh = ssfw.try_get_value('Scheme_V1', 'layout_horizontal')
214 | if lh:
215 | lh = tuple(map(lambda s:int(s), lh.split(',')))
216 | else:
217 | lh = None
218 |
219 | # 垂直拉升区域
220 | lv = ssfw.try_get_value('Scheme_V1', 'layout_vertical')
221 | if lv:
222 | lv = tuple(map(lambda s:int(s), lv.split(',')))
223 | else:
224 | lv = None
225 |
226 | # 拼音边距
227 | pinyin_marge = ssfw.try_get_value('Scheme_V1', 'pinyin_marge')
228 | if pinyin_marge:
229 | pinyin_marge = tuple(map(lambda s:int(s), pinyin_marge.split(',')))
230 | else:
231 | pinyin_marge = None
232 |
233 | # 候选词边距
234 | zhongwen_marge = ssfw.try_get_value('Scheme_V1', 'zhongwen_marge')
235 | if zhongwen_marge:
236 | zhongwen_marge = tuple(map(lambda s:int(s), zhongwen_marge.split(',')))
237 | else:
238 | zhongwen_marge = None
239 |
240 | if Scheme_V1_pic and lh and lv and pinyin_marge and zhongwen_marge:
241 | # 背景图片
242 | skin['Menu/Background'] = {
243 | 'Image': Scheme_V1_pic,
244 | }
245 |
246 | # 背景图片拉升边距
247 | skin['Menu/Background/Margin'] = {
248 | 'Left': lh[1],
249 | 'Right': lh[2],
250 | 'Top': lv[1],
251 | 'Bottom': lv[2],
252 | }
253 |
254 | sep = 1 if ssfw.try_get_value('Scheme_V1', 'separator') else 0
255 |
256 | # 背景图片内容边距
257 | horizontal_margin = min(zhongwen_marge[2], zhongwen_marge[3])
258 | skin['Menu/ContentMargin'] = {
259 | # 左边距
260 | 'Left': horizontal_margin,
261 |
262 | # 右边距
263 | 'Right': horizontal_margin,
264 |
265 | # 上边距
266 | 'Top': pinyin_marge[0] + pinyin_marge[1] + sep + zhongwen_marge[0],
267 |
268 | # 下边距
269 | 'Bottom': zhongwen_marge[1],
270 | }
271 | else:
272 | # 构建纯色背景
273 |
274 | # 用纯背景色构建出本主题的 menu.png
275 | img = Image.open(default_menu_img_bin)
276 | a = np.array(img)
277 | for i in range(len(a)):
278 | for j in range(len(a[0])):
279 | if a[i][j][3]:
280 | a[i][j][0] = back_color[0]
281 | a[i][j][1] = back_color[1]
282 | a[i][j][2] = back_color[2]
283 | img = Image.fromarray(a)
284 | img.save(skin_dir + os.sep + 'menu.png')
285 |
286 | # 背景图片
287 | skin['Menu/Background'] = {
288 | 'Image': 'menu.png',
289 | }
290 |
291 | # 背景图片拉升边距
292 | skin['Menu/Background/Margin'] = {
293 | 'Left': 20,
294 | 'Right': 20,
295 | 'Top': 20,
296 | 'Bottom': 20,
297 | }
298 |
299 | # 背景图片内容边距
300 | skin['Menu/ContentMargin'] = {
301 | # 左边距
302 | 'Left': 8,
303 |
304 | # 右边距
305 | 'Right': 8,
306 |
307 | # 上边距
308 | 'Top': 8,
309 |
310 | # 下边距
311 | 'Bottom': 8,
312 | }
313 |
314 | # 绘制高亮的透明图片
315 | #menu_highlight_color = rgbDistMax((255,255,255), back_color, input_color, first_color, other_color)
316 | Image.new('RGBA', (38,23), (0,0,0,0)).save(skin_dir + os.sep + 'menu_highlight.png')
317 |
318 | # 高亮背景
319 | skin['Menu/Highlight'] = {
320 | 'Image': 'menu_highlight.png',
321 | }
322 | # 高亮背景边距
323 | skin['Menu/Highlight/Margin'] = {
324 | 'Left': 10,
325 | 'Right': 10,
326 | 'Top': 5,
327 | 'Bottom': 5,
328 | }
329 |
330 | # 分隔符颜色
331 | skin['Menu/Separator'] = {
332 | 'Color': '#%02x%02x%02x' % other_color,
333 | }
334 |
335 | # 用纯背景色构建出本主题的 radio.png
336 | img = Image.open(default_radio_img_bin)
337 | a = np.array(img)
338 | for i in range(len(a)):
339 | for j in range(len(a[0])):
340 | if a[i][j][3]:
341 | a[i][j][0] = other_color[0]
342 | a[i][j][1] = other_color[1]
343 | a[i][j][2] = other_color[2]
344 | img = Image.fromarray(a)
345 | img.save(skin_dir + os.sep + 'radio.png')
346 |
347 | # 复选框图片
348 | skin['Menu/CheckBox'] = {
349 | 'Image': 'radio.png',
350 | }
351 |
352 | # 绘制箭头图片
353 | savePolygon((6,12), ((0,0),(6,6),(0,12)), other_color, skin_dir + os.sep + 'arrow.png')
354 |
355 | # 箭头图片
356 | skin['Menu/SubMenu'] = {
357 | 'Image': 'arrow.png',
358 | }
359 |
360 | # 菜单文字项边距
361 | skin['Menu/TextMargin'] = {
362 | # 左边距
363 | 'Left': 5,
364 |
365 | # 右边距
366 | 'Right': 5,
367 |
368 | # 上边距
369 | 'Top': 5,
370 |
371 | # 下边距
372 | 'Bottom': 5,
373 | }
374 |
375 | skin.write(open(skin_dir + os.sep + 'theme.conf', 'w', encoding="utf-8"), False)
376 | return 0
377 |
--------------------------------------------------------------------------------
/src/ssfconv/convert/out/__init__.py:
--------------------------------------------------------------------------------
1 | __all__ = ["Fcitx", "Fcitx5"]
2 |
3 | from .Fcitx import ssf2fcitx
4 | from .Fcitx5 import ssf2fcitx5
5 |
--------------------------------------------------------------------------------
/src/ssfconv/extract/__init__.py:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/ssfconv/extract/ssf.py:
--------------------------------------------------------------------------------
1 | from Cryptodome.Cipher import AES
2 | import zlib, struct
3 | from pathlib import Path
4 | import zipfile
5 |
6 | # 这部分最早可以追溯到 https://github.com/KDE/kimtoy/blob/master/kssf.cpp
7 |
8 | def extract_ssf(file_path:Path, dest_dir:Path):
9 | """
10 | 解压ssf文件到指定文件夹,文件夹不存在会自动创建
11 | ssf 文件格式目前有两种,一种是加密过后,一种未加密的zip
12 | """
13 | def __decrypt(bin):
14 | # AES 解密内容
15 | aesKey = b'\x52\x36\x46\x1A\xD3\x85\x03\x66' + \
16 | b'\x90\x45\x16\x28\x79\x03\x36\x23' + \
17 | b'\xDD\xBE\x6F\x03\xFF\x04\xE3\xCA' + \
18 | b'\xD5\x7F\xFC\xA3\x50\xE4\x9E\xD9'
19 | iv = b'\xE0\x7A\xAD\x35\xE0\x90\xAA\x03' + \
20 | b'\x8A\x51\xFD\x05\xDF\x8C\x5D\x0F'
21 | ssfAES = AES.new(aesKey, AES.MODE_CBC, iv)
22 | plain_bin = ssfAES.decrypt(bin[8:])
23 |
24 | # zlib 解压内容
25 | data = zlib.decompress(plain_bin[4:]) # 注意要跳过头四字节
26 |
27 | def readUint(offset):
28 | return struct.unpack('I', data[offset:offset+4])[0]
29 |
30 | # 整个内容的大小
31 | size = readUint(0)
32 |
33 | # 得到若干个偏移量
34 | offsets_size = readUint(4)
35 | offsets = struct.unpack('I'*(offsets_size//4),data[8:8+offsets_size])
36 |
37 | for offset in offsets:
38 | # 得到文件名
39 | name_len = readUint(offset)
40 | filename = data[offset+4:offset+4+name_len].decode('utf-16')
41 | # 得到文件内容
42 | content_len = readUint(offset+4+name_len)
43 | content = data[offset+8+name_len:offset+8+name_len+content_len]
44 |
45 | (dest_dir / filename).write_bytes(content)
46 |
47 | return
48 |
49 | ssf_bin = file_path.read_bytes()
50 |
51 | if ssf_bin[:4] == b'Skin': # 通过头四字节判断是否被加密
52 | __decrypt(ssf_bin)
53 | else:
54 | # 直接 zip 解压
55 | with zipfile.ZipFile(file_path) as zf:
56 | zf.extractall(dest_dir)
--------------------------------------------------------------------------------
/src/ssfconv/translation.py:
--------------------------------------------------------------------------------
1 | # import gettext, locale
2 |
3 | # localedir = Path() / "data" / "locales"
4 |
5 | # l10n = gettext.translation(
6 | # "ssfconv",
7 | # localedir=localedir,
8 | # fallback=True,
9 | # )
10 | # l10n.install()
11 | # _ = l10n.gettext
12 |
13 | #BUG https://github.com/breezy-team/setuptools-gettext/issues/94
14 | # I dont want to use deprecated setup.py so i18n attempt get stuck
15 | _ = lambda x:x #gettext placeholder
--------------------------------------------------------------------------------