@harmonyos-arkts/d2h 0.1.11 → 0.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +15 -13
- package/.claude-plugin/plugin.json +13 -7
- package/AGENTS.md +5 -5
- package/README.md +212 -212
- package/agents/android-to-hmos-00-orchestrator.md +318 -318
- package/agents/d2h.md +157 -157
- package/dist/bin/install-opencode.js +30 -30
- package/docs/environment-check.md +86 -86
- package/install-opencode.sh +160 -160
- package/opencode/agents.json +16 -16
- package/opencode/vscode-d2h-package.json +7 -7
- package/opencode/vscode-d2h-plugin.mjs +29 -29
- package/package.json +54 -54
- package/schemas/android-source-manifest.schema.json +25 -25
- package/schemas/checkpoint-provenance.schema.json +67 -67
- package/schemas/final-acceptance.schema.json +29 -29
- package/schemas/managed-evidence-index.schema.json +15 -15
- package/schemas/managed-evidence.schema.json +114 -114
- package/schemas/migration-config.schema.json +51 -51
- package/schemas/migration-report-index.schema.json +39 -39
- package/schemas/migration-report-item.schema.json +51 -51
- package/schemas/migration-report-summary.schema.json +25 -25
- package/schemas/migration-status.schema.json +202 -202
- package/schemas/preflight.schema.json +60 -60
- package/schemas/source-order-audit.schema.json +16 -16
- package/schemas/source-provenance-event.schema.json +19 -19
- package/schemas/spec-app-shard.schema.json +45 -45
- package/schemas/spec-fact-corrections-shard.schema.json +13 -13
- package/schemas/spec-features-shard.schema.json +43 -43
- package/schemas/spec-index.schema.json +26 -26
- package/schemas/spec-interactions-shard.schema.json +40 -40
- package/schemas/spec-page.schema.json +98 -98
- package/schemas/spec-pages-index.schema.json +1 -1
- package/schemas/spec-unresolved-shard.schema.json +1 -1
- package/schemas/task-envelope.schema.json +109 -109
- package/schemas/task-plan.schema.json +341 -341
- package/scripts/check-environment.mjs +320 -320
- package/skills/android2hmos_resources_convert/SKILL.md +180 -180
- package/skills/android2hmos_resources_convert/references/image-conversion-rules.md +230 -230
- package/skills/android2hmos_resources_convert/references/svg-fix-patterns.md +175 -175
- package/skills/android2hmos_resources_convert/references/xml-drawable-to-svg-rules.md +513 -513
- package/skills/arkts-app-identity/SKILL.md +238 -238
- package/skills/arkts-i18n/SKILL.md +496 -496
- package/skills/arkts-i18n/evals/evals.json +84 -84
- package/skills/arkts-i18n/references/code-examples.md +302 -302
- package/skills/arkts-i18n/references/common-pitfalls.md +391 -391
- package/skills/arkts-i18n/references/dynamic-language-switch.md +604 -604
- package/skills/arkts-i18n/references/hardcoded-string-scanner.md +348 -348
- package/skills/arkts-i18n/references/language-codes.md +104 -104
- package/skills/arkts-i18n/references/resource-file-structure.md +775 -775
- package/skills/arkts-i18n/references/static-vs-dynamic.md +242 -242
- package/skills/arkts-i18n/references/v1-compat.md +244 -244
- package/skills/arkts-i18n/scripts/audit_i18n_completeness.sh +174 -174
- package/skills/arkts-icon-sizing/SKILL.md +211 -211
- package/skills/arkts-icon-sizing/scripts/icon_audit.py +131 -131
- package/skills/arkts-icon-sizing/scripts/icon_autofix.py +88 -88
- package/skills/arkts-icon-sizing/scripts/icon_dims.py +179 -179
- package/skills/arkts-icon-sizing/scripts/icon_fix.py +119 -119
- package/skills/arkts-mvvm-architecture/SKILL.md +613 -613
- package/skills/d2h-hypium-forge/SKILL.md +309 -309
- package/skills/d2h-hypium-forge/references/functional-spec-template.md +140 -140
- package/skills/d2h-hypium-forge/references/official-api.md +135 -135
- package/skills/d2h-hypium-forge/references/pitfalls.md +98 -98
- package/skills/d2h-hypium-forge/references/testability-template.md +71 -71
- package/skills/d2h-hypium-forge/scripts/_common_template.py +659 -659
- package/skills/d2h-hypium-forge/scripts/analyze_results.py +165 -165
- package/skills/d2h-hypium-forge/scripts/assert_audit.py +107 -107
- package/skills/d2h-hypium-forge/scripts/collect_runtime_pages.py +169 -169
- package/skills/d2h-hypium-forge/scripts/forge_report.py +325 -325
- package/skills/d2h-hypium-forge/scripts/inject_resource_ids.py +114 -114
- package/skills/d2h-hypium-forge/scripts/lint_asserts.py +80 -80
- package/skills/d2h-hypium-forge/scripts/lint_locate.py +60 -60
- package/skills/d2h-hypium-forge/scripts/merge_appgraph.py +249 -249
- package/skills/d2h-hypium-forge/scripts/precondition.py +140 -140
- package/skills/d2h-hypium-forge/scripts/preinject_ids.py +232 -232
- package/skills/d2h-hypium-forge/scripts/probe.py +197 -197
- package/skills/d2h-hypium-forge/scripts/run_direct.py +823 -823
- package/skills/d2h-hypium-forge/scripts/run_parallel.py +121 -121
- package/skills/d2h-hypium-forge/scripts/scan_features.py +255 -255
- package/skills/d2h-hypium-forge/scripts/upgrade_asserts.py +66 -66
- package/skills/d2h-hypium-forge/scripts/validate_scenarios.py +84 -84
- package/skills/d2h-page-develop/SKILL.md +112 -112
- package/skills/d2h-page-develop/agents/openai.yaml +7 -7
- package/skills/d2h-page-spec-generate/SKILL.md +206 -206
- package/skills/d2h-page-spec-generate/scripts/build_batch_report.py +227 -227
- package/skills/d2h-utg-ai-generate/SKILL.md +128 -128
- package/skills/d2h-utg-ai-generate/references/click-paths-schema.json +76 -76
- package/skills/d2h-utg-ai-generate/references/extraction-guide.md +34 -34
- package/skills/d2h-utg-ai-generate/references/facts-contract.md +96 -96
- package/skills/d2h-utg-ai-generate/references/page-schema.json +61 -61
- package/skills/d2h-utg-ai-generate/references/utg-schema.json +89 -89
- package/skills/d2h-utg-ai-generate/scripts/build_page_json.py +314 -314
- package/skills/d2h-utg-ai-generate/scripts/build_utg.py +123 -123
- package/skills/d2h-utg-ai-generate/scripts/capture_from_page_json.ts +280 -280
- package/skills/d2h-utg-ai-generate/scripts/utg_common.py +524 -524
- package/skills/d2h-utg-ai-generate/scripts/utg_html.py +489 -489
- package/skills/d2h-utg-ai-generate/scripts/validate_utg.py +69 -69
- package/skills/d2h-utg-ai-generate/scripts/vendor/D3-LICENSE +13 -13
- package/skills/d2h-utg-ai-generate/scripts/vendor/d3-7.9.0.min.js +2 -2
- package/skills/d2h-utg-ai-generate/tests/test_pipeline.py +369 -369
- package/skills/d2h-utg-static-generate/SKILL.md +73 -73
- package/skills/d2h-utg-static-generate/references/discoveries.md +38 -38
- package/skills/d2h-utg-static-generate/scripts/generate.py +499 -499
- package/skills/d2h-utg-static-generate/tests/test_static.py +335 -335
- package/skills/d2h-utg-static-generate/tests/verify-offline-html.py +60 -60
- package/skills/d2h-utg-static-generate/tests/verify-produced-fixture.py +131 -131
- package/skills/harmonyos-migration-playbook/SKILL.md +83 -83
- package/skills/harmonyos-migration-playbook/agents/openai.yaml +7 -7
- package/skills/harmonyos-migration-playbook/references/arkts-compile.md +24 -24
- package/skills/harmonyos-migration-playbook/references/harmony-runtime.md +53 -53
- package/skills/harmonyos-migration-playbook/references/lesson-lifecycle.md +45 -45
- package/skills/harmonyos-migration-playbook/references/protocol-e2e.md +23 -23
- package/skills/harmonyos-migration-playbook/references/ui-automation.md +53 -53
- package/skills/harmonyos-migration-playbook/references/windows-environment.md +38 -38
- package/skills/maintaining-migration-report/SKILL.md +144 -144
- package/skills/maintaining-migration-report/references/final-acceptance.md +48 -48
- package/skills/maintaining-migration-report/references/source-provenance.md +66 -66
- package/skills/managing-d2h-toolchain/SKILL.md +24 -24
- package/skills/managing-d2h-toolchain/environment-requirements.json +16 -16
- package/skills/managing-d2h-toolchain/scripts/manage-toolchain.mjs +98 -98
- package/skills/managing-d2h-toolchain/toolchain.json +16 -16
- package/skills/native-library-substitution/SKILL.md +384 -384
- package/skills/native-library-substitution/references/native-library-substitution.json +56905 -56905
- package/skills/native-library-substitution/references/native-library-substitution.md +163 -163
- package/skills/preparing-migration-workspace/SKILL.md +131 -131
- package/skills/reviewing-migration-process/SKILL.md +62 -62
|
@@ -1,179 +1,179 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
# -*- coding: utf-8 -*-
|
|
3
|
-
"""第 2/3 步 —— 还原每个被标记图标的正确 vp 尺寸。
|
|
4
|
-
|
|
5
|
-
公式
|
|
6
|
-
HarmonyOS 中 `resources/base/media`(无密度限定符)里的位图按其字面像素尺寸
|
|
7
|
-
当作 vp 渲染。源位图来自某个 Android 密度桶(如 drawable-xxhdpi /
|
|
8
|
-
mipmap-xxhdpi = 3 倍)。因此图标被设计显示的尺寸是:
|
|
9
|
-
标称 vp = 源像素 / 源密度倍数
|
|
10
|
-
我们从 HarmonyOS media 文件读像素尺寸,从 Android 源目录名取倍数。宽和高
|
|
11
|
-
分别独立相除,所以真实宽高比得以保留(这正是修复「失真 / 比例错」的关键)。
|
|
12
|
-
|
|
13
|
-
倍数从何而来
|
|
14
|
-
1. 给了 --android-res 时,由图标的源目录(如 mipmap-xxhdpi)决定。Android
|
|
15
|
-
工程可能多模块 —— 把每个 res 根都传进来(如 app/src/main/res 与
|
|
16
|
-
base/src/main/res)。
|
|
17
|
-
2. 否则回退到 --density(默认 3.0 = xxhdpi,现代 app 最常见的单一桶)。输出
|
|
18
|
-
会标出 assumed 项,便于人工核对。
|
|
19
|
-
|
|
20
|
-
输出
|
|
21
|
-
打印表格并写出 icon_plan.json {icon: {w,h,px,density,src}}。
|
|
22
|
-
找不到 raster 的图标(动态引用、纯 SVG shape、矢量图)列为 UNRESOLVED 供手工处理。
|
|
23
|
-
|
|
24
|
-
用法
|
|
25
|
-
python icon_dims.py --report icon_report.json \
|
|
26
|
-
--media entry/src/main/resources/base/media \
|
|
27
|
-
--android-res /path/app/src/main/res /path/base/src/main/res
|
|
28
|
-
python icon_dims.py --report icon_report.json --density 3 # 无 Android 源
|
|
29
|
-
python icon_dims.py --media .../media --icons ic_a,ic_b # 临时查询
|
|
30
|
-
"""
|
|
31
|
-
import os, re, json, struct, glob, argparse
|
|
32
|
-
|
|
33
|
-
DENSITY = {'ldpi': 0.75, 'mdpi': 1.0, 'tvdpi': 1.33, 'hdpi': 1.5,
|
|
34
|
-
'xhdpi': 2.0, 'xxhdpi': 3.0, 'xxxhdpi': 4.0}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def webp_size(p):
|
|
38
|
-
d = open(p, 'rb').read(40)
|
|
39
|
-
if d[:4] != b'RIFF' or d[8:12] != b'WEBP':
|
|
40
|
-
return None
|
|
41
|
-
f = d[12:16]
|
|
42
|
-
if f == b'VP8 ':
|
|
43
|
-
return struct.unpack('<H', d[26:28])[0] & 0x3fff, struct.unpack('<H', d[28:30])[0] & 0x3fff
|
|
44
|
-
if f == b'VP8L':
|
|
45
|
-
b = d[21:25]
|
|
46
|
-
v = b[0] | b[1] << 8 | b[2] << 16 | b[3] << 24
|
|
47
|
-
return (v & 0x3fff) + 1, ((v >> 14) & 0x3fff) + 1
|
|
48
|
-
if f == b'VP8X':
|
|
49
|
-
return (d[24] | d[25] << 8 | d[26] << 16) + 1, (d[27] | d[28] << 8 | d[29] << 16) + 1
|
|
50
|
-
return None
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def png_size(p):
|
|
54
|
-
d = open(p, 'rb').read(24)
|
|
55
|
-
return struct.unpack('>II', d[16:24])
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def svg_size(p):
|
|
59
|
-
h = open(p, encoding='utf-8', errors='ignore').read(400)
|
|
60
|
-
m = re.search(r'width="([0-9.]+)"[^>]*height="([0-9.]+)"', h)
|
|
61
|
-
if m:
|
|
62
|
-
return float(m.group(1)), float(m.group(2))
|
|
63
|
-
m = re.search(r'viewBox="[\d.]+ [\d.]+ ([\d.]+) ([\d.]+)"', h)
|
|
64
|
-
return (float(m.group(1)), float(m.group(2))) if m else None
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def media_size(media_dir, name):
|
|
68
|
-
for ext, fn in (('.webp', webp_size), ('.png', png_size), ('.jpg', png_size), ('.svg', svg_size)):
|
|
69
|
-
p = os.path.join(media_dir, name + ext)
|
|
70
|
-
if os.path.exists(p):
|
|
71
|
-
try:
|
|
72
|
-
return fn(p), ext
|
|
73
|
-
except Exception:
|
|
74
|
-
return None, ext
|
|
75
|
-
return None, None
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
def src_multiplier(res_roots, name):
|
|
79
|
-
best = None
|
|
80
|
-
for root in res_roots:
|
|
81
|
-
for hit in glob.glob(f'{root}/*/{name}.*'):
|
|
82
|
-
folder = os.path.basename(os.path.dirname(hit))
|
|
83
|
-
for k, v in DENSITY.items():
|
|
84
|
-
if folder.endswith('-' + k) or folder.endswith(k):
|
|
85
|
-
if best is None or v > best[0]:
|
|
86
|
-
best = (v, folder)
|
|
87
|
-
return best
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
def find_media_dir():
|
|
91
|
-
# 排除非工程目录(skill 模板 / 依赖 / 构建产物 / 隐藏目录),避免 os.walk 误入
|
|
92
|
-
# .claude/skills/.../template/AppScope/resources/base/media 等 fixture。
|
|
93
|
-
skip = {'.git', '.hvigor', '.idea', '.claude', '.cxt',
|
|
94
|
-
'oh_modules', 'node_modules', 'build', 'template'}
|
|
95
|
-
candidates = []
|
|
96
|
-
for base, dirs, _ in os.walk('.'):
|
|
97
|
-
dirs[:] = [d for d in dirs if d not in skip and not d.startswith('.')]
|
|
98
|
-
if base.replace('\\', '/').endswith('resources/base/media'):
|
|
99
|
-
candidates.append(base)
|
|
100
|
-
if not candidates:
|
|
101
|
-
return None
|
|
102
|
-
# 优先工程模块媒体(*/src/main/resources/base/media)而非 app 级 AppScope 或其它
|
|
103
|
-
for c in candidates:
|
|
104
|
-
if '/src/main/resources/' in c.replace('\\', '/'):
|
|
105
|
-
return c
|
|
106
|
-
return candidates[0]
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
def build_plan(names, media, android_res, density):
|
|
110
|
-
"""对一组图标名,逐个 媒体像素 ÷ 源密度 算出标称 vp,返回 (plan, unresolved)。
|
|
111
|
-
|
|
112
|
-
被 icon_dims.main() 与 icon_autofix.py 共用。
|
|
113
|
-
names : 图标名列表(不含扩展名)
|
|
114
|
-
media : HarmonyOS base/media 目录
|
|
115
|
-
android_res : Android res 根列表(空/None → 用 density 回退)
|
|
116
|
-
density : 回退密度倍数
|
|
117
|
-
返回 plan = {icon: {w,h,px,density,src,ext}};unresolved = 找不到 raster 的图标名列表。
|
|
118
|
-
"""
|
|
119
|
-
plan, unresolved = {}, []
|
|
120
|
-
for ic in names:
|
|
121
|
-
sz, ext = media_size(media, ic)
|
|
122
|
-
if sz is None:
|
|
123
|
-
unresolved.append(ic)
|
|
124
|
-
continue
|
|
125
|
-
mult = src_multiplier(android_res, ic) if android_res else None
|
|
126
|
-
m = mult[0] if mult else density
|
|
127
|
-
src = mult[1] if mult else f'assumed {density}x'
|
|
128
|
-
w, h = sz
|
|
129
|
-
vw, vh = max(1, round(w / m)), max(1, round(h / m))
|
|
130
|
-
plan[ic] = {'w': vw, 'h': vh, 'px': [round(w), round(h)], 'density': m, 'src': src, 'ext': ext}
|
|
131
|
-
return plan, unresolved
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
def main():
|
|
135
|
-
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
136
|
-
ap.add_argument('--report', default='icon_report.json')
|
|
137
|
-
ap.add_argument('--media', help='HarmonyOS base/media 目录(默认:自动发现)')
|
|
138
|
-
ap.add_argument('--android-res', nargs='*', default=[], help='用于密度查找的 Android res 根')
|
|
139
|
-
ap.add_argument('--density', type=float, default=3.0, help='回退密度倍数(默认 3 = xxhdpi)')
|
|
140
|
-
ap.add_argument('--icons', help='逗号分隔的图标名,临时查询(替代 --report)')
|
|
141
|
-
ap.add_argument('--json', default='icon_plan.json')
|
|
142
|
-
a = ap.parse_args()
|
|
143
|
-
|
|
144
|
-
media = a.media or find_media_dir()
|
|
145
|
-
if not media:
|
|
146
|
-
print('未找到 resources/base/media —— 请用 --media 指定。')
|
|
147
|
-
return
|
|
148
|
-
|
|
149
|
-
if a.icons:
|
|
150
|
-
names = [n.strip() for n in a.icons.split(',') if n.strip()]
|
|
151
|
-
else:
|
|
152
|
-
rep = json.load(open(a.report, encoding='utf-8'))
|
|
153
|
-
names = sorted({f['icon'] for f in rep['flagged'] if f.get('icon')})
|
|
154
|
-
|
|
155
|
-
print(f"media: {media}")
|
|
156
|
-
print(f"密度来源: {a.android_res or f'回退 {a.density}x'}\n")
|
|
157
|
-
print(f"{'图标':40} {'像素':12} {'密度':16} {'-> vp (w x h)'}")
|
|
158
|
-
print('-' * 86)
|
|
159
|
-
plan, unresolved = build_plan(names, media, a.android_res or None, a.density)
|
|
160
|
-
for ic in names:
|
|
161
|
-
if ic in plan:
|
|
162
|
-
p = plan[ic]
|
|
163
|
-
px = f"{p['px'][0]}x{p['px'][1]}"
|
|
164
|
-
print(f"{ic:40} {px:12} {p['src']:16} -> {p['w']} x {p['h']}")
|
|
165
|
-
else:
|
|
166
|
-
label = '(无 raster)'
|
|
167
|
-
blank = ''
|
|
168
|
-
print(f"{ic:40} {label:12} {blank:16} -> 手工处理(动态 / svg shape / 矢量图)")
|
|
169
|
-
|
|
170
|
-
json.dump(plan, open(a.json, 'w', encoding='utf-8'), ensure_ascii=False, indent=2)
|
|
171
|
-
print(f'\n已写出 {a.json}({len(plan)} 个图标)。')
|
|
172
|
-
if unresolved:
|
|
173
|
-
print(f'UNRESOLVED({len(unresolved)})—— 手工解析资源后,用 --icons <名字> 重跑来测尺寸:'
|
|
174
|
-
f'{", ".join(unresolved)}')
|
|
175
|
-
print(f'请审核 icon_plan.json,然后:python icon_fix.py --plan {a.json} --report {a.report}(加 --apply 落盘)')
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
if __name__ == '__main__':
|
|
179
|
-
main()
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""第 2/3 步 —— 还原每个被标记图标的正确 vp 尺寸。
|
|
4
|
+
|
|
5
|
+
公式
|
|
6
|
+
HarmonyOS 中 `resources/base/media`(无密度限定符)里的位图按其字面像素尺寸
|
|
7
|
+
当作 vp 渲染。源位图来自某个 Android 密度桶(如 drawable-xxhdpi /
|
|
8
|
+
mipmap-xxhdpi = 3 倍)。因此图标被设计显示的尺寸是:
|
|
9
|
+
标称 vp = 源像素 / 源密度倍数
|
|
10
|
+
我们从 HarmonyOS media 文件读像素尺寸,从 Android 源目录名取倍数。宽和高
|
|
11
|
+
分别独立相除,所以真实宽高比得以保留(这正是修复「失真 / 比例错」的关键)。
|
|
12
|
+
|
|
13
|
+
倍数从何而来
|
|
14
|
+
1. 给了 --android-res 时,由图标的源目录(如 mipmap-xxhdpi)决定。Android
|
|
15
|
+
工程可能多模块 —— 把每个 res 根都传进来(如 app/src/main/res 与
|
|
16
|
+
base/src/main/res)。
|
|
17
|
+
2. 否则回退到 --density(默认 3.0 = xxhdpi,现代 app 最常见的单一桶)。输出
|
|
18
|
+
会标出 assumed 项,便于人工核对。
|
|
19
|
+
|
|
20
|
+
输出
|
|
21
|
+
打印表格并写出 icon_plan.json {icon: {w,h,px,density,src}}。
|
|
22
|
+
找不到 raster 的图标(动态引用、纯 SVG shape、矢量图)列为 UNRESOLVED 供手工处理。
|
|
23
|
+
|
|
24
|
+
用法
|
|
25
|
+
python icon_dims.py --report icon_report.json \
|
|
26
|
+
--media entry/src/main/resources/base/media \
|
|
27
|
+
--android-res /path/app/src/main/res /path/base/src/main/res
|
|
28
|
+
python icon_dims.py --report icon_report.json --density 3 # 无 Android 源
|
|
29
|
+
python icon_dims.py --media .../media --icons ic_a,ic_b # 临时查询
|
|
30
|
+
"""
|
|
31
|
+
import os, re, json, struct, glob, argparse
|
|
32
|
+
|
|
33
|
+
DENSITY = {'ldpi': 0.75, 'mdpi': 1.0, 'tvdpi': 1.33, 'hdpi': 1.5,
|
|
34
|
+
'xhdpi': 2.0, 'xxhdpi': 3.0, 'xxxhdpi': 4.0}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def webp_size(p):
|
|
38
|
+
d = open(p, 'rb').read(40)
|
|
39
|
+
if d[:4] != b'RIFF' or d[8:12] != b'WEBP':
|
|
40
|
+
return None
|
|
41
|
+
f = d[12:16]
|
|
42
|
+
if f == b'VP8 ':
|
|
43
|
+
return struct.unpack('<H', d[26:28])[0] & 0x3fff, struct.unpack('<H', d[28:30])[0] & 0x3fff
|
|
44
|
+
if f == b'VP8L':
|
|
45
|
+
b = d[21:25]
|
|
46
|
+
v = b[0] | b[1] << 8 | b[2] << 16 | b[3] << 24
|
|
47
|
+
return (v & 0x3fff) + 1, ((v >> 14) & 0x3fff) + 1
|
|
48
|
+
if f == b'VP8X':
|
|
49
|
+
return (d[24] | d[25] << 8 | d[26] << 16) + 1, (d[27] | d[28] << 8 | d[29] << 16) + 1
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def png_size(p):
|
|
54
|
+
d = open(p, 'rb').read(24)
|
|
55
|
+
return struct.unpack('>II', d[16:24])
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def svg_size(p):
|
|
59
|
+
h = open(p, encoding='utf-8', errors='ignore').read(400)
|
|
60
|
+
m = re.search(r'width="([0-9.]+)"[^>]*height="([0-9.]+)"', h)
|
|
61
|
+
if m:
|
|
62
|
+
return float(m.group(1)), float(m.group(2))
|
|
63
|
+
m = re.search(r'viewBox="[\d.]+ [\d.]+ ([\d.]+) ([\d.]+)"', h)
|
|
64
|
+
return (float(m.group(1)), float(m.group(2))) if m else None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def media_size(media_dir, name):
|
|
68
|
+
for ext, fn in (('.webp', webp_size), ('.png', png_size), ('.jpg', png_size), ('.svg', svg_size)):
|
|
69
|
+
p = os.path.join(media_dir, name + ext)
|
|
70
|
+
if os.path.exists(p):
|
|
71
|
+
try:
|
|
72
|
+
return fn(p), ext
|
|
73
|
+
except Exception:
|
|
74
|
+
return None, ext
|
|
75
|
+
return None, None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def src_multiplier(res_roots, name):
|
|
79
|
+
best = None
|
|
80
|
+
for root in res_roots:
|
|
81
|
+
for hit in glob.glob(f'{root}/*/{name}.*'):
|
|
82
|
+
folder = os.path.basename(os.path.dirname(hit))
|
|
83
|
+
for k, v in DENSITY.items():
|
|
84
|
+
if folder.endswith('-' + k) or folder.endswith(k):
|
|
85
|
+
if best is None or v > best[0]:
|
|
86
|
+
best = (v, folder)
|
|
87
|
+
return best
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def find_media_dir():
|
|
91
|
+
# 排除非工程目录(skill 模板 / 依赖 / 构建产物 / 隐藏目录),避免 os.walk 误入
|
|
92
|
+
# .claude/skills/.../template/AppScope/resources/base/media 等 fixture。
|
|
93
|
+
skip = {'.git', '.hvigor', '.idea', '.claude', '.cxt',
|
|
94
|
+
'oh_modules', 'node_modules', 'build', 'template'}
|
|
95
|
+
candidates = []
|
|
96
|
+
for base, dirs, _ in os.walk('.'):
|
|
97
|
+
dirs[:] = [d for d in dirs if d not in skip and not d.startswith('.')]
|
|
98
|
+
if base.replace('\\', '/').endswith('resources/base/media'):
|
|
99
|
+
candidates.append(base)
|
|
100
|
+
if not candidates:
|
|
101
|
+
return None
|
|
102
|
+
# 优先工程模块媒体(*/src/main/resources/base/media)而非 app 级 AppScope 或其它
|
|
103
|
+
for c in candidates:
|
|
104
|
+
if '/src/main/resources/' in c.replace('\\', '/'):
|
|
105
|
+
return c
|
|
106
|
+
return candidates[0]
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def build_plan(names, media, android_res, density):
|
|
110
|
+
"""对一组图标名,逐个 媒体像素 ÷ 源密度 算出标称 vp,返回 (plan, unresolved)。
|
|
111
|
+
|
|
112
|
+
被 icon_dims.main() 与 icon_autofix.py 共用。
|
|
113
|
+
names : 图标名列表(不含扩展名)
|
|
114
|
+
media : HarmonyOS base/media 目录
|
|
115
|
+
android_res : Android res 根列表(空/None → 用 density 回退)
|
|
116
|
+
density : 回退密度倍数
|
|
117
|
+
返回 plan = {icon: {w,h,px,density,src,ext}};unresolved = 找不到 raster 的图标名列表。
|
|
118
|
+
"""
|
|
119
|
+
plan, unresolved = {}, []
|
|
120
|
+
for ic in names:
|
|
121
|
+
sz, ext = media_size(media, ic)
|
|
122
|
+
if sz is None:
|
|
123
|
+
unresolved.append(ic)
|
|
124
|
+
continue
|
|
125
|
+
mult = src_multiplier(android_res, ic) if android_res else None
|
|
126
|
+
m = mult[0] if mult else density
|
|
127
|
+
src = mult[1] if mult else f'assumed {density}x'
|
|
128
|
+
w, h = sz
|
|
129
|
+
vw, vh = max(1, round(w / m)), max(1, round(h / m))
|
|
130
|
+
plan[ic] = {'w': vw, 'h': vh, 'px': [round(w), round(h)], 'density': m, 'src': src, 'ext': ext}
|
|
131
|
+
return plan, unresolved
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def main():
|
|
135
|
+
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
136
|
+
ap.add_argument('--report', default='icon_report.json')
|
|
137
|
+
ap.add_argument('--media', help='HarmonyOS base/media 目录(默认:自动发现)')
|
|
138
|
+
ap.add_argument('--android-res', nargs='*', default=[], help='用于密度查找的 Android res 根')
|
|
139
|
+
ap.add_argument('--density', type=float, default=3.0, help='回退密度倍数(默认 3 = xxhdpi)')
|
|
140
|
+
ap.add_argument('--icons', help='逗号分隔的图标名,临时查询(替代 --report)')
|
|
141
|
+
ap.add_argument('--json', default='icon_plan.json')
|
|
142
|
+
a = ap.parse_args()
|
|
143
|
+
|
|
144
|
+
media = a.media or find_media_dir()
|
|
145
|
+
if not media:
|
|
146
|
+
print('未找到 resources/base/media —— 请用 --media 指定。')
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
if a.icons:
|
|
150
|
+
names = [n.strip() for n in a.icons.split(',') if n.strip()]
|
|
151
|
+
else:
|
|
152
|
+
rep = json.load(open(a.report, encoding='utf-8'))
|
|
153
|
+
names = sorted({f['icon'] for f in rep['flagged'] if f.get('icon')})
|
|
154
|
+
|
|
155
|
+
print(f"media: {media}")
|
|
156
|
+
print(f"密度来源: {a.android_res or f'回退 {a.density}x'}\n")
|
|
157
|
+
print(f"{'图标':40} {'像素':12} {'密度':16} {'-> vp (w x h)'}")
|
|
158
|
+
print('-' * 86)
|
|
159
|
+
plan, unresolved = build_plan(names, media, a.android_res or None, a.density)
|
|
160
|
+
for ic in names:
|
|
161
|
+
if ic in plan:
|
|
162
|
+
p = plan[ic]
|
|
163
|
+
px = f"{p['px'][0]}x{p['px'][1]}"
|
|
164
|
+
print(f"{ic:40} {px:12} {p['src']:16} -> {p['w']} x {p['h']}")
|
|
165
|
+
else:
|
|
166
|
+
label = '(无 raster)'
|
|
167
|
+
blank = ''
|
|
168
|
+
print(f"{ic:40} {label:12} {blank:16} -> 手工处理(动态 / svg shape / 矢量图)")
|
|
169
|
+
|
|
170
|
+
json.dump(plan, open(a.json, 'w', encoding='utf-8'), ensure_ascii=False, indent=2)
|
|
171
|
+
print(f'\n已写出 {a.json}({len(plan)} 个图标)。')
|
|
172
|
+
if unresolved:
|
|
173
|
+
print(f'UNRESOLVED({len(unresolved)})—— 手工解析资源后,用 --icons <名字> 重跑来测尺寸:'
|
|
174
|
+
f'{", ".join(unresolved)}')
|
|
175
|
+
print(f'请审核 icon_plan.json,然后:python icon_fix.py --plan {a.json} --report {a.report}(加 --apply 落盘)')
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
if __name__ == '__main__':
|
|
179
|
+
main()
|
|
@@ -1,119 +1,119 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
# -*- coding: utf-8 -*-
|
|
3
|
-
"""第 3/3 步 —— 把还原出的尺寸插入 .ets 源码。
|
|
4
|
-
|
|
5
|
-
实时重扫(因此对行号位移健壮),对 icon_plan.json 中存在的每个欠约束静态图标:
|
|
6
|
-
- 既无 width 又无 height -> 插入 .width(W) .height(H) (固定标称 vp)
|
|
7
|
-
- 恰好只有一维 -> 插入 .aspectRatio(W/H) (保留有意的那一维,
|
|
8
|
-
如满宽横幅的 width('100%'),只修比例)
|
|
9
|
-
新修饰符插入在 `Image(...)` 开头之后、与原链同缩进,从而与 `.objectFit(...)` 等干净组合。
|
|
10
|
-
|
|
11
|
-
动态源图片(Image(this.foo())、三元 Image(cond ? a : b)、Builder `Image(param)`)
|
|
12
|
-
不自动改 —— 只报告,便于解析底层资源、用 icon_dims.py --icons 查尺寸后手工改。
|
|
13
|
-
|
|
14
|
-
安全
|
|
15
|
-
默认 dry-run(打印将要做的改动)。加 --apply 才落盘。
|
|
16
|
-
调用前记录允许修改的文件范围;需要恢复时只撤销本次输出清单中的修改,
|
|
17
|
-
不得暂存、提交或整体回滚调用方已有改动。
|
|
18
|
-
|
|
19
|
-
用法
|
|
20
|
-
python icon_fix.py --plan icon_plan.json --report icon_report.json # dry-run
|
|
21
|
-
python icon_fix.py --plan icon_plan.json --report icon_report.json --apply # 落盘
|
|
22
|
-
"""
|
|
23
|
-
import os, re, json, argparse
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def chain_end(lines, i):
|
|
27
|
-
base = len(lines[i]) - len(lines[i].lstrip())
|
|
28
|
-
j = i + 1
|
|
29
|
-
while j < len(lines):
|
|
30
|
-
if lines[j].strip() == '':
|
|
31
|
-
j += 1
|
|
32
|
-
continue
|
|
33
|
-
if (len(lines[j]) - len(lines[j].lstrip())) <= base:
|
|
34
|
-
break
|
|
35
|
-
j += 1
|
|
36
|
-
return j
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def apply_fixes(plan, report, apply=True):
|
|
40
|
-
"""对 report 标记的文件实时重扫,按 plan 给欠约束静态图标插入尺寸。
|
|
41
|
-
|
|
42
|
-
被 icon_fix.main() 与 icon_autofix.py 共用。
|
|
43
|
-
plan : {icon: {w,h,...}}(icon_dims.build_plan 产出)
|
|
44
|
-
report : icon_report.json 内容(取 flagged 的文件集合作为作用域)
|
|
45
|
-
apply : True 落盘,False 仅计算(dry-run)
|
|
46
|
-
返回 (applied, dynamic):applied 为修复描述行列表,dynamic 为动态源待手工项。
|
|
47
|
-
"""
|
|
48
|
-
files = sorted({f['file'] for f in report['flagged']}) # 仅针对被标记的文件
|
|
49
|
-
applied, dynamic = [], []
|
|
50
|
-
for path in files:
|
|
51
|
-
if not os.path.exists(path):
|
|
52
|
-
continue
|
|
53
|
-
lines = open(path, encoding='utf-8').read().split('\n')
|
|
54
|
-
edits = []
|
|
55
|
-
for i, ln in enumerate(lines):
|
|
56
|
-
st = ln.lstrip()
|
|
57
|
-
if st.startswith('*') or st.startswith('//') or st.startswith('/*'):
|
|
58
|
-
continue
|
|
59
|
-
code = ln.split('//')[0]
|
|
60
|
-
if not re.search(r'\bImage\(', code):
|
|
61
|
-
continue
|
|
62
|
-
end = chain_end(lines, i)
|
|
63
|
-
win = '\n'.join(lines[i + 1:end])
|
|
64
|
-
has_w, has_h = '.width(' in win, '.height(' in win
|
|
65
|
-
has_size, has_aspect = '.size(' in win, '.aspectRatio(' in win
|
|
66
|
-
has_weight = '.layoutWeight(' in win
|
|
67
|
-
w100, h100 = ".width('100%')" in win, ".height('100%')" in win
|
|
68
|
-
ok = has_size or (has_w and has_h) or ((has_w ^ has_h) and (has_aspect or has_weight))
|
|
69
|
-
if ok or (w100 and h100):
|
|
70
|
-
continue
|
|
71
|
-
m = re.search(r"(?:app|sys)\.media\.([A-Za-z0-9_]+)", code)
|
|
72
|
-
if not m:
|
|
73
|
-
dynamic.append(f'{path}:{i + 1} {st[:55]}')
|
|
74
|
-
continue
|
|
75
|
-
icon = m.group(1)
|
|
76
|
-
if icon not in plan:
|
|
77
|
-
continue
|
|
78
|
-
w, h = plan[icon]['w'], plan[icon]['h']
|
|
79
|
-
ind = ' ' * ((len(lines[i + 1]) - len(lines[i + 1].lstrip())) if i + 1 < end else (len(ln) - len(st)) + 2)
|
|
80
|
-
if not has_w and not has_h:
|
|
81
|
-
ins = [f'{ind}.width({w})', f'{ind}.height({h})']
|
|
82
|
-
kind = f'w/h {w}x{h}'
|
|
83
|
-
else:
|
|
84
|
-
ratio = round(w / h, 4)
|
|
85
|
-
ins = [f'{ind}.aspectRatio({ratio})']
|
|
86
|
-
kind = f'aspectRatio {ratio} ({"W" if has_w else "H"} set)'
|
|
87
|
-
edits.append((i, ins))
|
|
88
|
-
applied.append(f'{os.path.basename(path)}:{i + 1} {icon:34} -> {kind}')
|
|
89
|
-
if edits and apply:
|
|
90
|
-
for i, ins in sorted(edits, reverse=True):
|
|
91
|
-
lines[i + 1:i + 1] = ins
|
|
92
|
-
open(path, 'w', encoding='utf-8').write('\n'.join(lines))
|
|
93
|
-
return applied, dynamic
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
def main():
|
|
97
|
-
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
98
|
-
ap.add_argument('--plan', default='icon_plan.json')
|
|
99
|
-
ap.add_argument('--report', default='icon_report.json')
|
|
100
|
-
ap.add_argument('--apply', action='store_true', help='落盘(默认 dry-run)')
|
|
101
|
-
a = ap.parse_args()
|
|
102
|
-
|
|
103
|
-
plan = json.load(open(a.plan, encoding='utf-8'))
|
|
104
|
-
rep = json.load(open(a.report, encoding='utf-8'))
|
|
105
|
-
applied, dynamic = apply_fixes(plan, rep, apply=a.apply)
|
|
106
|
-
|
|
107
|
-
verb = '已落盘' if a.apply else '将要修复(dry-run)'
|
|
108
|
-
print(f'== {verb}:{len(applied)} 处 ==')
|
|
109
|
-
for s in sorted(applied):
|
|
110
|
-
print(' ' + s)
|
|
111
|
-
print(f'\n== 动态源(手工解析,见 SKILL.md):{len(dynamic)} ==')
|
|
112
|
-
for d in dynamic:
|
|
113
|
-
print(' ' + d)
|
|
114
|
-
if not a.apply:
|
|
115
|
-
print('\n仅 dry-run。加 --apply 落盘,然后重跑 icon_audit.py + 编译。')
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if __name__ == '__main__':
|
|
119
|
-
main()
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""第 3/3 步 —— 把还原出的尺寸插入 .ets 源码。
|
|
4
|
+
|
|
5
|
+
实时重扫(因此对行号位移健壮),对 icon_plan.json 中存在的每个欠约束静态图标:
|
|
6
|
+
- 既无 width 又无 height -> 插入 .width(W) .height(H) (固定标称 vp)
|
|
7
|
+
- 恰好只有一维 -> 插入 .aspectRatio(W/H) (保留有意的那一维,
|
|
8
|
+
如满宽横幅的 width('100%'),只修比例)
|
|
9
|
+
新修饰符插入在 `Image(...)` 开头之后、与原链同缩进,从而与 `.objectFit(...)` 等干净组合。
|
|
10
|
+
|
|
11
|
+
动态源图片(Image(this.foo())、三元 Image(cond ? a : b)、Builder `Image(param)`)
|
|
12
|
+
不自动改 —— 只报告,便于解析底层资源、用 icon_dims.py --icons 查尺寸后手工改。
|
|
13
|
+
|
|
14
|
+
安全
|
|
15
|
+
默认 dry-run(打印将要做的改动)。加 --apply 才落盘。
|
|
16
|
+
调用前记录允许修改的文件范围;需要恢复时只撤销本次输出清单中的修改,
|
|
17
|
+
不得暂存、提交或整体回滚调用方已有改动。
|
|
18
|
+
|
|
19
|
+
用法
|
|
20
|
+
python icon_fix.py --plan icon_plan.json --report icon_report.json # dry-run
|
|
21
|
+
python icon_fix.py --plan icon_plan.json --report icon_report.json --apply # 落盘
|
|
22
|
+
"""
|
|
23
|
+
import os, re, json, argparse
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def chain_end(lines, i):
|
|
27
|
+
base = len(lines[i]) - len(lines[i].lstrip())
|
|
28
|
+
j = i + 1
|
|
29
|
+
while j < len(lines):
|
|
30
|
+
if lines[j].strip() == '':
|
|
31
|
+
j += 1
|
|
32
|
+
continue
|
|
33
|
+
if (len(lines[j]) - len(lines[j].lstrip())) <= base:
|
|
34
|
+
break
|
|
35
|
+
j += 1
|
|
36
|
+
return j
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def apply_fixes(plan, report, apply=True):
|
|
40
|
+
"""对 report 标记的文件实时重扫,按 plan 给欠约束静态图标插入尺寸。
|
|
41
|
+
|
|
42
|
+
被 icon_fix.main() 与 icon_autofix.py 共用。
|
|
43
|
+
plan : {icon: {w,h,...}}(icon_dims.build_plan 产出)
|
|
44
|
+
report : icon_report.json 内容(取 flagged 的文件集合作为作用域)
|
|
45
|
+
apply : True 落盘,False 仅计算(dry-run)
|
|
46
|
+
返回 (applied, dynamic):applied 为修复描述行列表,dynamic 为动态源待手工项。
|
|
47
|
+
"""
|
|
48
|
+
files = sorted({f['file'] for f in report['flagged']}) # 仅针对被标记的文件
|
|
49
|
+
applied, dynamic = [], []
|
|
50
|
+
for path in files:
|
|
51
|
+
if not os.path.exists(path):
|
|
52
|
+
continue
|
|
53
|
+
lines = open(path, encoding='utf-8').read().split('\n')
|
|
54
|
+
edits = []
|
|
55
|
+
for i, ln in enumerate(lines):
|
|
56
|
+
st = ln.lstrip()
|
|
57
|
+
if st.startswith('*') or st.startswith('//') or st.startswith('/*'):
|
|
58
|
+
continue
|
|
59
|
+
code = ln.split('//')[0]
|
|
60
|
+
if not re.search(r'\bImage\(', code):
|
|
61
|
+
continue
|
|
62
|
+
end = chain_end(lines, i)
|
|
63
|
+
win = '\n'.join(lines[i + 1:end])
|
|
64
|
+
has_w, has_h = '.width(' in win, '.height(' in win
|
|
65
|
+
has_size, has_aspect = '.size(' in win, '.aspectRatio(' in win
|
|
66
|
+
has_weight = '.layoutWeight(' in win
|
|
67
|
+
w100, h100 = ".width('100%')" in win, ".height('100%')" in win
|
|
68
|
+
ok = has_size or (has_w and has_h) or ((has_w ^ has_h) and (has_aspect or has_weight))
|
|
69
|
+
if ok or (w100 and h100):
|
|
70
|
+
continue
|
|
71
|
+
m = re.search(r"(?:app|sys)\.media\.([A-Za-z0-9_]+)", code)
|
|
72
|
+
if not m:
|
|
73
|
+
dynamic.append(f'{path}:{i + 1} {st[:55]}')
|
|
74
|
+
continue
|
|
75
|
+
icon = m.group(1)
|
|
76
|
+
if icon not in plan:
|
|
77
|
+
continue
|
|
78
|
+
w, h = plan[icon]['w'], plan[icon]['h']
|
|
79
|
+
ind = ' ' * ((len(lines[i + 1]) - len(lines[i + 1].lstrip())) if i + 1 < end else (len(ln) - len(st)) + 2)
|
|
80
|
+
if not has_w and not has_h:
|
|
81
|
+
ins = [f'{ind}.width({w})', f'{ind}.height({h})']
|
|
82
|
+
kind = f'w/h {w}x{h}'
|
|
83
|
+
else:
|
|
84
|
+
ratio = round(w / h, 4)
|
|
85
|
+
ins = [f'{ind}.aspectRatio({ratio})']
|
|
86
|
+
kind = f'aspectRatio {ratio} ({"W" if has_w else "H"} set)'
|
|
87
|
+
edits.append((i, ins))
|
|
88
|
+
applied.append(f'{os.path.basename(path)}:{i + 1} {icon:34} -> {kind}')
|
|
89
|
+
if edits and apply:
|
|
90
|
+
for i, ins in sorted(edits, reverse=True):
|
|
91
|
+
lines[i + 1:i + 1] = ins
|
|
92
|
+
open(path, 'w', encoding='utf-8').write('\n'.join(lines))
|
|
93
|
+
return applied, dynamic
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def main():
|
|
97
|
+
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
98
|
+
ap.add_argument('--plan', default='icon_plan.json')
|
|
99
|
+
ap.add_argument('--report', default='icon_report.json')
|
|
100
|
+
ap.add_argument('--apply', action='store_true', help='落盘(默认 dry-run)')
|
|
101
|
+
a = ap.parse_args()
|
|
102
|
+
|
|
103
|
+
plan = json.load(open(a.plan, encoding='utf-8'))
|
|
104
|
+
rep = json.load(open(a.report, encoding='utf-8'))
|
|
105
|
+
applied, dynamic = apply_fixes(plan, rep, apply=a.apply)
|
|
106
|
+
|
|
107
|
+
verb = '已落盘' if a.apply else '将要修复(dry-run)'
|
|
108
|
+
print(f'== {verb}:{len(applied)} 处 ==')
|
|
109
|
+
for s in sorted(applied):
|
|
110
|
+
print(' ' + s)
|
|
111
|
+
print(f'\n== 动态源(手工解析,见 SKILL.md):{len(dynamic)} ==')
|
|
112
|
+
for d in dynamic:
|
|
113
|
+
print(' ' + d)
|
|
114
|
+
if not a.apply:
|
|
115
|
+
print('\n仅 dry-run。加 --apply 落盘,然后重跑 icon_audit.py + 编译。')
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
if __name__ == '__main__':
|
|
119
|
+
main()
|