@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.
Files changed (126) hide show
  1. package/.claude-plugin/marketplace.json +15 -13
  2. package/.claude-plugin/plugin.json +13 -7
  3. package/AGENTS.md +5 -5
  4. package/README.md +212 -212
  5. package/agents/android-to-hmos-00-orchestrator.md +318 -318
  6. package/agents/d2h.md +157 -157
  7. package/dist/bin/install-opencode.js +30 -30
  8. package/docs/environment-check.md +86 -86
  9. package/install-opencode.sh +160 -160
  10. package/opencode/agents.json +16 -16
  11. package/opencode/vscode-d2h-package.json +7 -7
  12. package/opencode/vscode-d2h-plugin.mjs +29 -29
  13. package/package.json +54 -54
  14. package/schemas/android-source-manifest.schema.json +25 -25
  15. package/schemas/checkpoint-provenance.schema.json +67 -67
  16. package/schemas/final-acceptance.schema.json +29 -29
  17. package/schemas/managed-evidence-index.schema.json +15 -15
  18. package/schemas/managed-evidence.schema.json +114 -114
  19. package/schemas/migration-config.schema.json +51 -51
  20. package/schemas/migration-report-index.schema.json +39 -39
  21. package/schemas/migration-report-item.schema.json +51 -51
  22. package/schemas/migration-report-summary.schema.json +25 -25
  23. package/schemas/migration-status.schema.json +202 -202
  24. package/schemas/preflight.schema.json +60 -60
  25. package/schemas/source-order-audit.schema.json +16 -16
  26. package/schemas/source-provenance-event.schema.json +19 -19
  27. package/schemas/spec-app-shard.schema.json +45 -45
  28. package/schemas/spec-fact-corrections-shard.schema.json +13 -13
  29. package/schemas/spec-features-shard.schema.json +43 -43
  30. package/schemas/spec-index.schema.json +26 -26
  31. package/schemas/spec-interactions-shard.schema.json +40 -40
  32. package/schemas/spec-page.schema.json +98 -98
  33. package/schemas/spec-pages-index.schema.json +1 -1
  34. package/schemas/spec-unresolved-shard.schema.json +1 -1
  35. package/schemas/task-envelope.schema.json +109 -109
  36. package/schemas/task-plan.schema.json +341 -341
  37. package/scripts/check-environment.mjs +320 -320
  38. package/skills/android2hmos_resources_convert/SKILL.md +180 -180
  39. package/skills/android2hmos_resources_convert/references/image-conversion-rules.md +230 -230
  40. package/skills/android2hmos_resources_convert/references/svg-fix-patterns.md +175 -175
  41. package/skills/android2hmos_resources_convert/references/xml-drawable-to-svg-rules.md +513 -513
  42. package/skills/arkts-app-identity/SKILL.md +238 -238
  43. package/skills/arkts-i18n/SKILL.md +496 -496
  44. package/skills/arkts-i18n/evals/evals.json +84 -84
  45. package/skills/arkts-i18n/references/code-examples.md +302 -302
  46. package/skills/arkts-i18n/references/common-pitfalls.md +391 -391
  47. package/skills/arkts-i18n/references/dynamic-language-switch.md +604 -604
  48. package/skills/arkts-i18n/references/hardcoded-string-scanner.md +348 -348
  49. package/skills/arkts-i18n/references/language-codes.md +104 -104
  50. package/skills/arkts-i18n/references/resource-file-structure.md +775 -775
  51. package/skills/arkts-i18n/references/static-vs-dynamic.md +242 -242
  52. package/skills/arkts-i18n/references/v1-compat.md +244 -244
  53. package/skills/arkts-i18n/scripts/audit_i18n_completeness.sh +174 -174
  54. package/skills/arkts-icon-sizing/SKILL.md +211 -211
  55. package/skills/arkts-icon-sizing/scripts/icon_audit.py +131 -131
  56. package/skills/arkts-icon-sizing/scripts/icon_autofix.py +88 -88
  57. package/skills/arkts-icon-sizing/scripts/icon_dims.py +179 -179
  58. package/skills/arkts-icon-sizing/scripts/icon_fix.py +119 -119
  59. package/skills/arkts-mvvm-architecture/SKILL.md +613 -613
  60. package/skills/d2h-hypium-forge/SKILL.md +309 -309
  61. package/skills/d2h-hypium-forge/references/functional-spec-template.md +140 -140
  62. package/skills/d2h-hypium-forge/references/official-api.md +135 -135
  63. package/skills/d2h-hypium-forge/references/pitfalls.md +98 -98
  64. package/skills/d2h-hypium-forge/references/testability-template.md +71 -71
  65. package/skills/d2h-hypium-forge/scripts/_common_template.py +659 -659
  66. package/skills/d2h-hypium-forge/scripts/analyze_results.py +165 -165
  67. package/skills/d2h-hypium-forge/scripts/assert_audit.py +107 -107
  68. package/skills/d2h-hypium-forge/scripts/collect_runtime_pages.py +169 -169
  69. package/skills/d2h-hypium-forge/scripts/forge_report.py +325 -325
  70. package/skills/d2h-hypium-forge/scripts/inject_resource_ids.py +114 -114
  71. package/skills/d2h-hypium-forge/scripts/lint_asserts.py +80 -80
  72. package/skills/d2h-hypium-forge/scripts/lint_locate.py +60 -60
  73. package/skills/d2h-hypium-forge/scripts/merge_appgraph.py +249 -249
  74. package/skills/d2h-hypium-forge/scripts/precondition.py +140 -140
  75. package/skills/d2h-hypium-forge/scripts/preinject_ids.py +232 -232
  76. package/skills/d2h-hypium-forge/scripts/probe.py +197 -197
  77. package/skills/d2h-hypium-forge/scripts/run_direct.py +823 -823
  78. package/skills/d2h-hypium-forge/scripts/run_parallel.py +121 -121
  79. package/skills/d2h-hypium-forge/scripts/scan_features.py +255 -255
  80. package/skills/d2h-hypium-forge/scripts/upgrade_asserts.py +66 -66
  81. package/skills/d2h-hypium-forge/scripts/validate_scenarios.py +84 -84
  82. package/skills/d2h-page-develop/SKILL.md +112 -112
  83. package/skills/d2h-page-develop/agents/openai.yaml +7 -7
  84. package/skills/d2h-page-spec-generate/SKILL.md +206 -206
  85. package/skills/d2h-page-spec-generate/scripts/build_batch_report.py +227 -227
  86. package/skills/d2h-utg-ai-generate/SKILL.md +128 -128
  87. package/skills/d2h-utg-ai-generate/references/click-paths-schema.json +76 -76
  88. package/skills/d2h-utg-ai-generate/references/extraction-guide.md +34 -34
  89. package/skills/d2h-utg-ai-generate/references/facts-contract.md +96 -96
  90. package/skills/d2h-utg-ai-generate/references/page-schema.json +61 -61
  91. package/skills/d2h-utg-ai-generate/references/utg-schema.json +89 -89
  92. package/skills/d2h-utg-ai-generate/scripts/build_page_json.py +314 -314
  93. package/skills/d2h-utg-ai-generate/scripts/build_utg.py +123 -123
  94. package/skills/d2h-utg-ai-generate/scripts/capture_from_page_json.ts +280 -280
  95. package/skills/d2h-utg-ai-generate/scripts/utg_common.py +524 -524
  96. package/skills/d2h-utg-ai-generate/scripts/utg_html.py +489 -489
  97. package/skills/d2h-utg-ai-generate/scripts/validate_utg.py +69 -69
  98. package/skills/d2h-utg-ai-generate/scripts/vendor/D3-LICENSE +13 -13
  99. package/skills/d2h-utg-ai-generate/scripts/vendor/d3-7.9.0.min.js +2 -2
  100. package/skills/d2h-utg-ai-generate/tests/test_pipeline.py +369 -369
  101. package/skills/d2h-utg-static-generate/SKILL.md +73 -73
  102. package/skills/d2h-utg-static-generate/references/discoveries.md +38 -38
  103. package/skills/d2h-utg-static-generate/scripts/generate.py +499 -499
  104. package/skills/d2h-utg-static-generate/tests/test_static.py +335 -335
  105. package/skills/d2h-utg-static-generate/tests/verify-offline-html.py +60 -60
  106. package/skills/d2h-utg-static-generate/tests/verify-produced-fixture.py +131 -131
  107. package/skills/harmonyos-migration-playbook/SKILL.md +83 -83
  108. package/skills/harmonyos-migration-playbook/agents/openai.yaml +7 -7
  109. package/skills/harmonyos-migration-playbook/references/arkts-compile.md +24 -24
  110. package/skills/harmonyos-migration-playbook/references/harmony-runtime.md +53 -53
  111. package/skills/harmonyos-migration-playbook/references/lesson-lifecycle.md +45 -45
  112. package/skills/harmonyos-migration-playbook/references/protocol-e2e.md +23 -23
  113. package/skills/harmonyos-migration-playbook/references/ui-automation.md +53 -53
  114. package/skills/harmonyos-migration-playbook/references/windows-environment.md +38 -38
  115. package/skills/maintaining-migration-report/SKILL.md +144 -144
  116. package/skills/maintaining-migration-report/references/final-acceptance.md +48 -48
  117. package/skills/maintaining-migration-report/references/source-provenance.md +66 -66
  118. package/skills/managing-d2h-toolchain/SKILL.md +24 -24
  119. package/skills/managing-d2h-toolchain/environment-requirements.json +16 -16
  120. package/skills/managing-d2h-toolchain/scripts/manage-toolchain.mjs +98 -98
  121. package/skills/managing-d2h-toolchain/toolchain.json +16 -16
  122. package/skills/native-library-substitution/SKILL.md +384 -384
  123. package/skills/native-library-substitution/references/native-library-substitution.json +56905 -56905
  124. package/skills/native-library-substitution/references/native-library-substitution.md +163 -163
  125. package/skills/preparing-migration-workspace/SKILL.md +131 -131
  126. 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()