@lark-apaas/coding-steering 0.1.31 → 0.1.32-beta.0
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/package.json +1 -1
- package/steering/design-html/skills/pptx-style-extract/SKILL.md +112 -0
- package/steering/design-html/skills/pptx-style-extract/font-fallback.yaml +129 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/census.py +955 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/check_v2.py +907 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/draft.py +945 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/export_consumer_md.py +75 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/export_consumer_zip.py +175 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/extract.py +765 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/ooxml.py +699 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/package.py +1120 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/parts.py +461 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/query.py +562 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/render_pages.py +679 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/verify_font.py +68 -0
- package/steering/design-html/skills/pptx-style-extract/v2-format-spec.md +193 -0
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""L 层查询入口:按需从阶段一产物里捞数,避免整读 extract.json / shapes.json。
|
|
3
|
+
|
|
4
|
+
python3 query.py <stage1-outdir> <子命令> [选项]
|
|
5
|
+
|
|
6
|
+
shapes --part slide7.xml [--kind text|pic|shape|table|group] [--ph] [--limit N]
|
|
7
|
+
colors [--top N] [--class design|editor|aux]
|
|
8
|
+
fonts [--all] 默认只列 rendered_n>0 的族
|
|
9
|
+
text-scale [--top N]
|
|
10
|
+
images [--fullscreen] [--repeat] [--top N]
|
|
11
|
+
clusters [--multi] --multi 只看跨文件同素材簇
|
|
12
|
+
media [--candidate]
|
|
13
|
+
summary 阶段一体检:form/themes/UNRESOLVED/降级信号
|
|
14
|
+
get <点路径> 取 extract.json 任意字段(如 get canvas)
|
|
15
|
+
slides 每页背景 + 版式
|
|
16
|
+
layouts 版式清单
|
|
17
|
+
|
|
18
|
+
输出是给 agent 读的紧凑表格:定宽列 + 表头,数值直接可抄进产物。
|
|
19
|
+
`shapes` 读 ref/shapes.json(体量大,务必带 --part 过滤),其余读 extract.json。
|
|
20
|
+
"""
|
|
21
|
+
import argparse
|
|
22
|
+
import json
|
|
23
|
+
import math
|
|
24
|
+
import os
|
|
25
|
+
import sys
|
|
26
|
+
from collections import Counter
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def load(outdir, name):
|
|
30
|
+
p = os.path.join(outdir, name)
|
|
31
|
+
if not os.path.exists(p):
|
|
32
|
+
raise SystemExit('query.py: 找不到 %s' % p)
|
|
33
|
+
with open(p, encoding='utf-8') as f:
|
|
34
|
+
return json.load(f)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def short(part):
|
|
38
|
+
return part.rsplit('/', 1)[-1] if part else '-'
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def trunc(s, n):
|
|
42
|
+
s = '' if s is None else str(s).replace('\n', ' ')
|
|
43
|
+
return s if len(s) <= n else s[:n - 1] + '…'
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def table(rows, headers, aligns=None):
|
|
47
|
+
if not rows:
|
|
48
|
+
print('(无匹配)')
|
|
49
|
+
return
|
|
50
|
+
cols = len(headers)
|
|
51
|
+
w = [len(str(h)) for h in headers]
|
|
52
|
+
for r in rows:
|
|
53
|
+
for i in range(cols):
|
|
54
|
+
w[i] = max(w[i], len(str(r[i])))
|
|
55
|
+
aligns = aligns or ['<'] * cols
|
|
56
|
+
fmt = ' '.join('{:%s%d}' % (aligns[i], w[i]) for i in range(cols))
|
|
57
|
+
print(fmt.format(*headers))
|
|
58
|
+
print(' '.join('-' * w[i] for i in range(cols)))
|
|
59
|
+
for r in rows:
|
|
60
|
+
print(fmt.format(*[('' if x is None else x) for x in r]))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# ------------------------------------------------------------------- 子命令
|
|
64
|
+
KIND_GROUPS = {'text': None, 'pic': ('pic',), 'shape': ('sp', 'cxnSp'),
|
|
65
|
+
'table': ('table',), 'group': ('grpSp',)}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def cmd_shapes(a, outdir):
|
|
69
|
+
data = load(outdir, os.path.join('ref', 'shapes.json'))
|
|
70
|
+
shapes = data['shapes'] if isinstance(data, dict) else data
|
|
71
|
+
if a.part:
|
|
72
|
+
want = a.part
|
|
73
|
+
shapes = [s for s in shapes if short(s['part']) == want or s['part'] == want]
|
|
74
|
+
if not shapes:
|
|
75
|
+
parts = sorted({short(s['part']) for s in (data['shapes'] if isinstance(data, dict) else data)})
|
|
76
|
+
raise SystemExit('query.py: 没有 part=%s。可选: %s' % (want, ', '.join(parts[:12]) + ' …'))
|
|
77
|
+
if a.kind:
|
|
78
|
+
if a.kind == 'text':
|
|
79
|
+
shapes = [s for s in shapes if (s.get('text') or {}).get('paragraphs')]
|
|
80
|
+
else:
|
|
81
|
+
keep = KIND_GROUPS[a.kind]
|
|
82
|
+
shapes = [s for s in shapes if s.get('kind') in keep]
|
|
83
|
+
if a.ph:
|
|
84
|
+
shapes = [s for s in shapes if s.get('ph')]
|
|
85
|
+
rows = []
|
|
86
|
+
for s in shapes[:a.limit]:
|
|
87
|
+
b = s.get('box') or {}
|
|
88
|
+
txt = ''
|
|
89
|
+
for p in (s.get('text') or {}).get('paragraphs', []):
|
|
90
|
+
txt += ''.join(r.get('text') or '' for r in p.get('runs', []))
|
|
91
|
+
if len(txt) > 40:
|
|
92
|
+
break
|
|
93
|
+
# 有效字号按 OOXML 优先级找第一个有声明的层:run > 段落 defRPr > 本形状 lstStyle
|
|
94
|
+
text = s.get('text') or {}
|
|
95
|
+
sz = next((r.get('sz_px') for p in text.get('paragraphs', [])
|
|
96
|
+
for r in p.get('runs', []) if r.get('sz_px')), None)
|
|
97
|
+
if sz is None:
|
|
98
|
+
sz = next((p['defRPr']['sz_px'] for p in text.get('paragraphs', [])
|
|
99
|
+
if (p.get('defRPr') or {}).get('sz_px')), None)
|
|
100
|
+
if sz is None:
|
|
101
|
+
sz = next((lvl.get('sz_px') for lvl in (text.get('lstStyle') or {}).values()
|
|
102
|
+
if lvl.get('sz_px')), None)
|
|
103
|
+
fill = (s.get('fill') or {}).get('type')
|
|
104
|
+
col = ((s.get('fill') or {}).get('color') or {}).get('resolved')
|
|
105
|
+
rows.append([s.get('kind'), s.get('id'), trunc(s.get('name'), 22),
|
|
106
|
+
'%s/%s' % (s['ph']['type'], s['ph'].get('idx')) if s.get('ph') else '-',
|
|
107
|
+
b.get('x'), b.get('y'), b.get('w'), b.get('h'),
|
|
108
|
+
sz or '-', (fill or '-') + (('=' + col) if col else ''),
|
|
109
|
+
trunc(txt, 34)])
|
|
110
|
+
table(rows, ['kind', 'id', 'name', 'ph', 'x', 'y', 'w', 'h', 'sz', 'fill', 'text'],
|
|
111
|
+
['<', '>', '<', '<', '>', '>', '>', '>', '>', '<', '<'])
|
|
112
|
+
print('\n%d 个形状%s' % (len(shapes), '(截断到 %d)' % a.limit if len(shapes) > a.limit else ''))
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def cmd_colors(a, outdir):
|
|
116
|
+
d = load(outdir, 'extract.json')
|
|
117
|
+
freq = d['color_freq']
|
|
118
|
+
if a.klass:
|
|
119
|
+
freq = [c for c in freq if c['class'] == a.klass]
|
|
120
|
+
rows = [[c.get('resolved'), c['n'], c['class'],
|
|
121
|
+
trunc(','.join(c.get('raw') or []), 40),
|
|
122
|
+
trunc(','.join('%s:%s' % kv for kv in (c.get('layers') or {}).items()), 26)]
|
|
123
|
+
for c in freq[:a.top]]
|
|
124
|
+
table(rows, ['resolved', 'n', 'class', 'raw tokens', 'layers'],
|
|
125
|
+
['<', '>', '<', '<', '<'])
|
|
126
|
+
print('\n共 %d 支色(design %d / editor %d / aux %d)'
|
|
127
|
+
% (len(d['color_freq']),
|
|
128
|
+
sum(1 for c in d['color_freq'] if c['class'] == 'design'),
|
|
129
|
+
sum(1 for c in d['color_freq'] if c['class'] == 'editor'),
|
|
130
|
+
sum(1 for c in d['color_freq'] if c['class'] == 'aux')))
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def cmd_fonts(a, outdir):
|
|
134
|
+
d = load(outdir, 'extract.json')
|
|
135
|
+
fams = d['font_families']
|
|
136
|
+
if not a.all:
|
|
137
|
+
fams = [f for f in fams if f.get('rendered_n', f['n'])]
|
|
138
|
+
rows = [[f['family'], f['n'], f.get('rendered_n', f['n']),
|
|
139
|
+
','.join(str(w) for w in f.get('weights') or []) or '-',
|
|
140
|
+
f.get('bold_runs', 0),
|
|
141
|
+
trunc(','.join('%s:%s' % kv for kv in (f.get('sources') or {}).items()), 34),
|
|
142
|
+
'Y' if f.get('in_theme') else '',
|
|
143
|
+
'renders_no_text' if f.get('renders_no_text') else '']
|
|
144
|
+
for f in fams]
|
|
145
|
+
table(rows, ['family', 'n', 'rendered', 'weights', 'bold', 'sources', 'theme', 'flag'],
|
|
146
|
+
['<', '>', '>', '<', '>', '<', '<', '<'])
|
|
147
|
+
print('\n提示:判「在用」看 rendered —— n 含空段落声明(不渲染文字)。'
|
|
148
|
+
' 全部族用 --all。')
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def cmd_text_scale(a, outdir):
|
|
152
|
+
d = load(outdir, 'extract.json')
|
|
153
|
+
rows = [[e['sz_px'], e['n'],
|
|
154
|
+
trunc(','.join('%s:%s' % kv for kv in (e.get('sources') or {}).items()), 34),
|
|
155
|
+
trunc(','.join('%s:%s' % kv for kv in (e.get('layers') or {}).items()), 24),
|
|
156
|
+
','.join(str(w) for w in (e.get('weights') or {})) or '-',
|
|
157
|
+
trunc(','.join(str(k) for k in (e.get('line_height_mult') or {})), 18)]
|
|
158
|
+
for e in d['text_scale'][:a.top]]
|
|
159
|
+
table(rows, ['sz_px', 'n', 'sources', 'layers', 'weights', 'lnSpc'],
|
|
160
|
+
['>', '>', '<', '<', '<', '<'])
|
|
161
|
+
print('\n共 %d 档字号。sources=pPr 是段落级 defRPr(Mac Office 常用)。'
|
|
162
|
+
% len(d['text_scale']))
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def cmd_images(a, outdir):
|
|
166
|
+
d = load(outdir, 'extract.json')
|
|
167
|
+
imgs = d['images']
|
|
168
|
+
if a.fullscreen:
|
|
169
|
+
imgs = [i for i in imgs if i.get('fullscreen')]
|
|
170
|
+
if a.repeat:
|
|
171
|
+
imgs = [i for i in imgs if i.get('repeat_fixed')]
|
|
172
|
+
rows = []
|
|
173
|
+
for i in imgs[:a.top]:
|
|
174
|
+
b = (i.get('boxes') or [{}])[0].get('box') or {}
|
|
175
|
+
dom = ' '.join('%s' % c['hex'] for c in (i.get('dominant_colors') or [])[:3])
|
|
176
|
+
rows.append([short(i['media']), i['n'], len(i.get('boxes') or []),
|
|
177
|
+
b.get('x'), b.get('y'), b.get('w'), b.get('h'),
|
|
178
|
+
'Y' if i.get('fullscreen') else '',
|
|
179
|
+
len(i.get('repeat_fixed') or []),
|
|
180
|
+
','.join(i.get('variant_group') or []) or '-',
|
|
181
|
+
i.get('content_id') or '-',
|
|
182
|
+
i.get('luminance') if i.get('luminance') is not None else '-', dom])
|
|
183
|
+
table(rows, ['media', 'n', 'clus', 'x', 'y', 'w', 'h', 'full', 'rep',
|
|
184
|
+
'vgroup', 'cid', 'lum', 'dominant'],
|
|
185
|
+
['<', '>', '>', '>', '>', '>', '>', '<', '>', '<', '<', '>', '<'])
|
|
186
|
+
print('\n共 %d 张被引用素材(满屏 %d / 有固定重复位 %d)'
|
|
187
|
+
% (len(d['images']), sum(1 for i in d['images'] if i.get('fullscreen')),
|
|
188
|
+
sum(1 for i in d['images'] if i.get('repeat_fixed'))))
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
# ----------------------------------------------------- L11 配方(现成 CSS)
|
|
192
|
+
def _css_color(c):
|
|
193
|
+
"""{resolved} 已是 #HEX 或 rgba(...),直接用。"""
|
|
194
|
+
if not isinstance(c, dict):
|
|
195
|
+
return None
|
|
196
|
+
return c.get('resolved') or c.get('hex')
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _css_gradient(g):
|
|
200
|
+
"""OOXML gradFill → CSS linear-gradient。ang 自 +x 轴顺时针,CSS 自 12 点,故 +90。"""
|
|
201
|
+
stops = sorted(g.get('stops') or [], key=lambda s: s.get('pos', 0))
|
|
202
|
+
if not stops:
|
|
203
|
+
return None
|
|
204
|
+
parts = ['%s %g%%' % (_css_color(s.get('color')) or 'transparent', s.get('pos', 0))
|
|
205
|
+
for s in stops]
|
|
206
|
+
return 'linear-gradient(%gdeg, %s)' % ((g.get('angle_deg') or 0) + 90, ', '.join(parts))
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _fill_css(f):
|
|
210
|
+
if not isinstance(f, dict):
|
|
211
|
+
return None, None
|
|
212
|
+
t = f.get('type')
|
|
213
|
+
if t == 'solid':
|
|
214
|
+
c = _css_color(f.get('color'))
|
|
215
|
+
return ('background: %s' % c, c) if c else (None, None)
|
|
216
|
+
if t == 'gradient':
|
|
217
|
+
g = _css_gradient(f)
|
|
218
|
+
return ('background-image: %s' % g, g) if g else (None, None)
|
|
219
|
+
if t == 'image':
|
|
220
|
+
return 'background-image: url(%s)' % (f.get('media') or '').rsplit('/', 1)[-1], None
|
|
221
|
+
if t == 'none':
|
|
222
|
+
return 'background: transparent', 'transparent'
|
|
223
|
+
return None, None
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _recipe_css(fill, line, radii, effects):
|
|
227
|
+
"""一组形状 → 可直接粘贴的 CSS 声明块。"""
|
|
228
|
+
out = []
|
|
229
|
+
fill_decl, fill_val = _fill_css(fill)
|
|
230
|
+
line_grad = _css_gradient(line['gradient']) if (line or {}).get('gradient') else None
|
|
231
|
+
w = int(round((line or {}).get('w_px') or 0)) if line and not (line or {}).get('none') else 0
|
|
232
|
+
if line_grad and w:
|
|
233
|
+
# 渐变描边只能用双背景实现:内层填充走 padding-box,描边渐变走 border-box
|
|
234
|
+
base = fill_val if fill_val and fill_val.startswith('linear-gradient') else \
|
|
235
|
+
'linear-gradient(%s, %s)' % (fill_val or 'transparent', fill_val or 'transparent')
|
|
236
|
+
out.append('border: %dpx solid transparent' % w)
|
|
237
|
+
out.append('background: %s padding-box,\n %s border-box'
|
|
238
|
+
% (base, line_grad))
|
|
239
|
+
else:
|
|
240
|
+
if fill_decl:
|
|
241
|
+
out.append(fill_decl)
|
|
242
|
+
if w:
|
|
243
|
+
lc = _css_color((line or {}).get('color')) or 'currentColor'
|
|
244
|
+
dash = (line or {}).get('dash')
|
|
245
|
+
style = 'dashed' if dash and 'dash' in dash else 'solid'
|
|
246
|
+
out.append('border: %dpx %s %s' % (w, style, lc))
|
|
247
|
+
if radii:
|
|
248
|
+
lo, hi = min(radii), max(radii)
|
|
249
|
+
if abs(hi - lo) <= 0.5:
|
|
250
|
+
out.append('border-radius: %gpx' % round(lo, 1))
|
|
251
|
+
else:
|
|
252
|
+
out.append('border-radius: %gpx\x00/* 源内 %g~%gpx 共 %d 档,归一档位由 L11 定 */'
|
|
253
|
+
% (round(sum(radii) / len(radii), 1), round(lo, 1), round(hi, 1),
|
|
254
|
+
len(set(round(r, 1) for r in radii))))
|
|
255
|
+
for e in effects or []:
|
|
256
|
+
if e.get('type') == 'outerShdw':
|
|
257
|
+
col = _css_color(e.get('color')) or 'rgba(0,0,0,0.25)'
|
|
258
|
+
blur = e.get('blurRad_px') or 0
|
|
259
|
+
dist = e.get('dist_px') or 0
|
|
260
|
+
ang = e.get('dir_deg') or 0
|
|
261
|
+
dx = dist * math.cos(math.radians(ang))
|
|
262
|
+
dy = dist * math.sin(math.radians(ang))
|
|
263
|
+
out.append('box-shadow: %.1fpx %.1fpx %.1fpx %s' % (dx, dy, blur, col))
|
|
264
|
+
return out
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _sig(fill, line, effects):
|
|
268
|
+
"""分组键 = 填充 + 描边 + 效果。**不含圆角**——OOXML 圆角是 min(w,h) 的百分比,
|
|
269
|
+
同一配方在不同尺寸的卡上绝对 px 必然不同(feishu 三张 glassCard 实测 10.8/12.8/19.5),
|
|
270
|
+
把它计入键会把一个配方拆成三组;归一到哪一档是 L11 的判断,脚本只报区间。"""
|
|
271
|
+
f = 'none'
|
|
272
|
+
if isinstance(fill, dict):
|
|
273
|
+
if fill.get('type') == 'solid':
|
|
274
|
+
f = 'solid:%s' % _css_color(fill.get('color'))
|
|
275
|
+
elif fill.get('type') == 'gradient':
|
|
276
|
+
f = 'grad:%s' % _css_gradient(fill)
|
|
277
|
+
else:
|
|
278
|
+
f = fill.get('type') or 'none'
|
|
279
|
+
ln = 'none'
|
|
280
|
+
if isinstance(line, dict) and not line.get('none'):
|
|
281
|
+
if line.get('gradient'):
|
|
282
|
+
ln = 'grad:%g:%s' % (line.get('w_px') or 0, _css_gradient(line['gradient']))
|
|
283
|
+
elif line.get('color'):
|
|
284
|
+
ln = '%g:%s:%s' % (line.get('w_px') or 0, _css_color(line['color']),
|
|
285
|
+
line.get('dash') or '')
|
|
286
|
+
fx = ','.join(sorted(e.get('type', '') for e in effects or []))
|
|
287
|
+
return (f, ln, fx)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def cmd_recipes(a, outdir):
|
|
291
|
+
data = load(outdir, os.path.join('ref', 'shapes.json'))
|
|
292
|
+
shapes = data['shapes'] if isinstance(data, dict) else data
|
|
293
|
+
groups = {}
|
|
294
|
+
for s in shapes:
|
|
295
|
+
fill, line = s.get('fill'), s.get('line')
|
|
296
|
+
fx = s.get('effects')
|
|
297
|
+
radius = s.get('radius_px')
|
|
298
|
+
if not fill and not line and not fx:
|
|
299
|
+
continue
|
|
300
|
+
if isinstance(fill, dict) and fill.get('type') == 'image' and not a.all:
|
|
301
|
+
continue # 图片填充是素材不是配方
|
|
302
|
+
k = _sig(fill, line, fx)
|
|
303
|
+
if k[0] == 'none' and k[1] == 'none' and not k[2]:
|
|
304
|
+
continue
|
|
305
|
+
g = groups.setdefault(k, {'n': 0, 'parts': Counter(), 'sizes': [], 'radii': [],
|
|
306
|
+
'fill': fill, 'line': line, 'fx': fx})
|
|
307
|
+
g['n'] += 1
|
|
308
|
+
if radius:
|
|
309
|
+
g['radii'].append(radius)
|
|
310
|
+
g['parts'][short(s['part'])] += 1
|
|
311
|
+
b = s.get('box') or {}
|
|
312
|
+
if b.get('w'):
|
|
313
|
+
g['sizes'].append((b['w'], b['h']))
|
|
314
|
+
rows = sorted(groups.values(), key=lambda g: -g['n'])
|
|
315
|
+
rows = [g for g in rows if g['n'] >= a.min]
|
|
316
|
+
if not rows:
|
|
317
|
+
print('(无满足 --min %d 的配方组)' % a.min)
|
|
318
|
+
return
|
|
319
|
+
for i, g in enumerate(rows[:a.top], 1):
|
|
320
|
+
css = _recipe_css(g['fill'], g['line'], g['radii'], g['fx'])
|
|
321
|
+
pages = ', '.join('%s×%d' % (p, n) if n > 1 else p
|
|
322
|
+
for p, n in g['parts'].most_common(6))
|
|
323
|
+
if len(g['parts']) > 6:
|
|
324
|
+
pages += ' …共 %d 处' % len(g['parts'])
|
|
325
|
+
sz = ''
|
|
326
|
+
if g['sizes']:
|
|
327
|
+
ws = sorted(set(round(w) for w, _ in g['sizes']))
|
|
328
|
+
hs = sorted(set(round(h) for _, h in g['sizes']))
|
|
329
|
+
sz = ' 尺寸 w%s h%s' % (ws[0] if len(ws) == 1 else '%d~%d' % (ws[0], ws[-1]),
|
|
330
|
+
hs[0] if len(hs) == 1 else '%d~%d' % (hs[0], hs[-1]))
|
|
331
|
+
print('[r%d] 出现 %d 次%s' % (i, g['n'], sz))
|
|
332
|
+
print(' 页: %s' % pages)
|
|
333
|
+
for decl in css:
|
|
334
|
+
sub = decl.split('\n')
|
|
335
|
+
for j, ln in enumerate(sub):
|
|
336
|
+
if j == len(sub) - 1:
|
|
337
|
+
head, _, note = ln.partition('\x00')
|
|
338
|
+
print(' %s;%s' % (head, (' ' + note) if note else ''))
|
|
339
|
+
else:
|
|
340
|
+
print(' %s' % ln)
|
|
341
|
+
print()
|
|
342
|
+
print('共 %d 组配方(出现 ≥%d 次)。命名与取舍由 L11 判断,CSS 已可直接粘贴。'
|
|
343
|
+
% (len(rows), a.min))
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def cmd_grids(a, outdir):
|
|
347
|
+
d = load(outdir, 'extract.json')
|
|
348
|
+
grids = (d.get('spacing_candidates') or {}).get('grids') or []
|
|
349
|
+
rows = []
|
|
350
|
+
for g in grids:
|
|
351
|
+
c, r = g.get('cols'), g.get('rows')
|
|
352
|
+
fmt = lambda x: ('%d @%gpx' % (x['n'], x['pitch']) if x['regular']
|
|
353
|
+
else '(%d @%gpx 不规整)' % (x['n'], x['pitch'])) if x else '-'
|
|
354
|
+
rows.append([short(g['part']), g['kind'], g['n'],
|
|
355
|
+
fmt(c), c['pitch_stdev'] if c else '-',
|
|
356
|
+
fmt(r), r['pitch_stdev'] if r else '-',
|
|
357
|
+
'%s/%s' % (g.get('filled', '-'), g.get('cells', '-')),
|
|
358
|
+
'%g~%g' % tuple(g['item_w']), '%g~%g' % tuple(g['item_h'])])
|
|
359
|
+
table(rows, ['part', 'kind', 'n', 'cols', 'c-sd', 'rows', 'r-sd',
|
|
360
|
+
'filled/cells', 'item w', 'item h'],
|
|
361
|
+
['<', '<', '>', '<', '>', '<', '>', '<', '<', '<'])
|
|
362
|
+
if a.part:
|
|
363
|
+
for g in grids:
|
|
364
|
+
if short(g['part']) != a.part:
|
|
365
|
+
continue
|
|
366
|
+
print('\n%s %s 详情:' % (short(g['part']), g['kind']))
|
|
367
|
+
for ax in ('cols', 'rows'):
|
|
368
|
+
if ax in g:
|
|
369
|
+
x = g[ax]
|
|
370
|
+
print(' %s n=%d pitch=%g stdev=%g regular=%s'
|
|
371
|
+
% (ax, x['n'], x['pitch'], x['pitch_stdev'], x['regular']))
|
|
372
|
+
print(' 中心 %s' % x['centers'])
|
|
373
|
+
print(' 起点 %s' % x['starts'])
|
|
374
|
+
print('\n%d 组栅格。中心分档(非左上角)——同格内元素尺寸可不同仍算同列。'
|
|
375
|
+
'带「不规整」的轴步距方差超阈值,只作参考不要照抄。' % len(grids))
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def cmd_clusters(a, outdir):
|
|
379
|
+
d = load(outdir, 'extract.json')
|
|
380
|
+
cl = d.get('media_clusters') or []
|
|
381
|
+
if a.multi:
|
|
382
|
+
cl = [c for c in cl if c['member_n'] > 1]
|
|
383
|
+
rows = []
|
|
384
|
+
for c in cl:
|
|
385
|
+
b = (c.get('boxes') or [{}])[0]
|
|
386
|
+
rows.append([c['content_id'], c['member_n'], c['n'],
|
|
387
|
+
'Y' if c.get('sha256_identical') else '',
|
|
388
|
+
len(c.get('repeat_fixed') or []),
|
|
389
|
+
len(c.get('repeat_fixed_cross_media') or []),
|
|
390
|
+
trunc(','.join(short(m) for m in c['members']), 52)])
|
|
391
|
+
table(rows, ['cid', 'files', 'n', 'sha=', 'rep', 'xrep', 'members'],
|
|
392
|
+
['<', '>', '>', '<', '>', '>', '<'])
|
|
393
|
+
print('\n%d 簇(跨文件同素材 %d 簇)。xrep>0 = 同素材散成多文件却占同一位置。'
|
|
394
|
+
% (len(d.get('media_clusters') or []),
|
|
395
|
+
sum(1 for c in (d.get('media_clusters') or []) if c['member_n'] > 1)))
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def cmd_media(a, outdir):
|
|
399
|
+
d = load(outdir, 'extract.json')
|
|
400
|
+
rows = []
|
|
401
|
+
for m in d['media']:
|
|
402
|
+
if a.candidate and not m.get('candidate'):
|
|
403
|
+
continue
|
|
404
|
+
rows.append([short(m['media']), m['ext'], m['bytes'],
|
|
405
|
+
'Y' if m.get('candidate') else '', m.get('used_n', 0),
|
|
406
|
+
short(m.get('out')) if m.get('exported') else '-',
|
|
407
|
+
short(m.get('compressed_out')) or '-',
|
|
408
|
+
m.get('compressed_bytes') or '-',
|
|
409
|
+
trunc(','.join(m.get('reasons') or []), 34)])
|
|
410
|
+
table(rows, ['media', 'ext', 'bytes', 'cand', 'used', 'out', 'compressed',
|
|
411
|
+
'comp_bytes', 'reasons'],
|
|
412
|
+
['<', '<', '>', '<', '>', '<', '<', '>', '<'])
|
|
413
|
+
print('\n%d 个 media,导出 %d,转码 %d。压缩图 = compressed 列那份,原图 = out 列。'
|
|
414
|
+
% (len(d['media']), d['counts'].get('media_exported', 0),
|
|
415
|
+
d['counts'].get('media_transcoded', 0)))
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def cmd_slides(a, outdir):
|
|
419
|
+
d = load(outdir, 'extract.json')
|
|
420
|
+
rows = []
|
|
421
|
+
for s in d.get('slides') or []:
|
|
422
|
+
bg = s.get('background') or {}
|
|
423
|
+
col = (bg.get('color') or {}).get('resolved')
|
|
424
|
+
rows.append([short(s['part']), short(s.get('layout')),
|
|
425
|
+
bg.get('type') or (bg.get('source') or '-'),
|
|
426
|
+
col or '-',
|
|
427
|
+
len(bg.get('stops') or []) or '-'])
|
|
428
|
+
table(rows, ['slide', 'layout', 'bg type', 'bg color', 'stops'])
|
|
429
|
+
print('\n%d 页实例页' % len(d.get('slides') or []))
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def cmd_layouts(a, outdir):
|
|
433
|
+
d = load(outdir, 'extract.json')
|
|
434
|
+
rows = []
|
|
435
|
+
for l in d['layouts']:
|
|
436
|
+
bg = l.get('background') or {}
|
|
437
|
+
rows.append([short(l['part']), trunc(l.get('name'), 26), l.get('type_attr'),
|
|
438
|
+
short(l.get('master')), l.get('used_by_slides'), l.get('shape_n'),
|
|
439
|
+
trunc(','.join('%s:%s' % kv for kv in (l.get('placeholders') or {}).items()), 30),
|
|
440
|
+
(bg.get('color') or {}).get('resolved') or bg.get('type') or '-'])
|
|
441
|
+
table(rows, ['layout', 'name', 'type', 'master', 'used', 'shapes', 'placeholders', 'bg'],
|
|
442
|
+
['<', '<', '<', '<', '>', '>', '<', '<'])
|
|
443
|
+
print('\n%d 个版式;form_hint=%s(%s)'
|
|
444
|
+
% (len(d['layouts']), d['form_hint']['form'], d['form_hint']['note']))
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def cmd_summary(a, outdir):
|
|
449
|
+
d = load(outdir, 'extract.json')
|
|
450
|
+
fh = d.get('form_hint') or {}
|
|
451
|
+
tt = d.get('theme_topology') or {}
|
|
452
|
+
counts = d.get('counts') or {}
|
|
453
|
+
cv = d.get('canvas') or {}
|
|
454
|
+
print('canvas : %s px 源 %s EMU' % (cv.get('px'), (cv.get('source') or {}).get('cx')))
|
|
455
|
+
print('form : %s (%s)' % (fh.get('form'), trunc(fh.get('note'), 72)))
|
|
456
|
+
print('themes : %s mechanism=%s' % (tt.get('themes'), tt.get('mechanism')))
|
|
457
|
+
print('slides : %s layouts %s / masters %s' % (
|
|
458
|
+
counts.get('slides'), counts.get('layouts'), counts.get('masters')))
|
|
459
|
+
print('colors : %s 条 fonts %s 族 images %s' % (
|
|
460
|
+
len(d.get('color_freq') or []), len(d.get('font_families') or []),
|
|
461
|
+
len(d.get('images') or [])))
|
|
462
|
+
multi = [c for c in d.get('media_clusters') or [] if len(c.get('members') or []) > 1]
|
|
463
|
+
if multi:
|
|
464
|
+
print('clusters : %d 个跨文件同素材簇(query.py clusters --multi 细看)' % len(multi))
|
|
465
|
+
warn = 0
|
|
466
|
+
unresolved = sum(c.get('n', 0) for c in d.get('color_freq') or []
|
|
467
|
+
if 'UNRESOLVED' in str(c.get('resolved', '')))
|
|
468
|
+
if unresolved:
|
|
469
|
+
warn += 1
|
|
470
|
+
print('⚠ UNRESOLVED 色 %d 次——schemeClr 解析有残留,查 ref/ 定位,不能带进语义层' % unresolved)
|
|
471
|
+
if not d.get('pillow_available', True):
|
|
472
|
+
warn += 1
|
|
473
|
+
print('⚠ Pillow 不可用——聚类/取色/转码已降级(%s),产物记 gaps' % d.get('content_cluster_mode'))
|
|
474
|
+
if d.get('scheme_fallback'):
|
|
475
|
+
print('note: scheme_fallback 触发 %d 次(rels 断链回退,详见 extract.json)' % len(d['scheme_fallback']))
|
|
476
|
+
if not warn:
|
|
477
|
+
print('OK: 无阻塞信号,可进语义层')
|
|
478
|
+
return 0
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def cmd_get(a, outdir):
|
|
483
|
+
"""按点路径取 extract.json 任意字段,如 get canvas / get theme_topology.mechanism"""
|
|
484
|
+
d = load(outdir, 'extract.json')
|
|
485
|
+
cur = d
|
|
486
|
+
for seg in a.path.split('.'):
|
|
487
|
+
if isinstance(cur, list):
|
|
488
|
+
cur = cur[int(seg)]
|
|
489
|
+
elif isinstance(cur, dict):
|
|
490
|
+
if seg not in cur:
|
|
491
|
+
raise SystemExit('get: 无字段 %s(同级键: %s)' % (seg, ', '.join(sorted(cur)[:20])))
|
|
492
|
+
cur = cur[seg]
|
|
493
|
+
else:
|
|
494
|
+
raise SystemExit('get: %s 已是标量' % seg)
|
|
495
|
+
print(json.dumps(cur, ensure_ascii=False, indent=1)[:20000])
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def main(argv=None):
|
|
499
|
+
ap = argparse.ArgumentParser(description=__doc__,
|
|
500
|
+
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
501
|
+
ap.add_argument('outdir')
|
|
502
|
+
sub = ap.add_subparsers(dest='cmd', required=True)
|
|
503
|
+
|
|
504
|
+
p = sub.add_parser('shapes')
|
|
505
|
+
p.add_argument('--part')
|
|
506
|
+
p.add_argument('--kind', choices=sorted(KIND_GROUPS))
|
|
507
|
+
p.add_argument('--ph', action='store_true')
|
|
508
|
+
p.add_argument('--limit', type=int, default=200)
|
|
509
|
+
p.set_defaults(fn=cmd_shapes)
|
|
510
|
+
|
|
511
|
+
p = sub.add_parser('colors')
|
|
512
|
+
p.add_argument('--top', type=int, default=40)
|
|
513
|
+
p.add_argument('--class', dest='klass', choices=('design', 'editor', 'aux'))
|
|
514
|
+
p.set_defaults(fn=cmd_colors)
|
|
515
|
+
|
|
516
|
+
p = sub.add_parser('fonts')
|
|
517
|
+
p.add_argument('--all', action='store_true')
|
|
518
|
+
p.set_defaults(fn=cmd_fonts)
|
|
519
|
+
|
|
520
|
+
p = sub.add_parser('text-scale')
|
|
521
|
+
p.add_argument('--top', type=int, default=60)
|
|
522
|
+
p.set_defaults(fn=cmd_text_scale)
|
|
523
|
+
|
|
524
|
+
p = sub.add_parser('images')
|
|
525
|
+
p.add_argument('--fullscreen', action='store_true')
|
|
526
|
+
p.add_argument('--repeat', action='store_true')
|
|
527
|
+
p.add_argument('--top', type=int, default=60)
|
|
528
|
+
p.set_defaults(fn=cmd_images)
|
|
529
|
+
|
|
530
|
+
p = sub.add_parser('clusters')
|
|
531
|
+
p.add_argument('--multi', action='store_true')
|
|
532
|
+
p.set_defaults(fn=cmd_clusters)
|
|
533
|
+
|
|
534
|
+
p = sub.add_parser('media')
|
|
535
|
+
p.add_argument('--candidate', action='store_true')
|
|
536
|
+
p.set_defaults(fn=cmd_media)
|
|
537
|
+
|
|
538
|
+
sub.add_parser('summary').set_defaults(fn=cmd_summary)
|
|
539
|
+
|
|
540
|
+
p = sub.add_parser('get')
|
|
541
|
+
p.add_argument('path')
|
|
542
|
+
p.set_defaults(fn=cmd_get)
|
|
543
|
+
p = sub.add_parser('recipes')
|
|
544
|
+
p.add_argument('--min', type=int, default=2)
|
|
545
|
+
p.add_argument('--top', type=int, default=20)
|
|
546
|
+
p.add_argument('--all', action='store_true')
|
|
547
|
+
p.set_defaults(fn=cmd_recipes)
|
|
548
|
+
|
|
549
|
+
p = sub.add_parser('grids')
|
|
550
|
+
p.add_argument('--part')
|
|
551
|
+
p.set_defaults(fn=cmd_grids)
|
|
552
|
+
|
|
553
|
+
sub.add_parser('slides').set_defaults(fn=cmd_slides)
|
|
554
|
+
sub.add_parser('layouts').set_defaults(fn=cmd_layouts)
|
|
555
|
+
|
|
556
|
+
a = ap.parse_args(argv)
|
|
557
|
+
a.fn(a, os.path.abspath(a.outdir))
|
|
558
|
+
return 0
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
if __name__ == '__main__':
|
|
562
|
+
sys.exit(main())
|