@lark-apaas/coding-steering 0.1.18-dev.655b398 → 0.1.18-dev.7f786ca
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/charts/SKILL.md +4 -0
- package/steering/design-html/skills/pptx-style-extract/SKILL.md +44 -22
- package/steering/design-html/skills/pptx-style-extract/scripts/check_v2.py +140 -2
- package/steering/design-html/skills/pptx-style-extract/scripts/draft.py +1276 -177
- package/steering/design-html/skills/pptx-style-extract/scripts/extract.py +277 -15
- package/steering/design-html/skills/pptx-style-extract/scripts/ooxml.py +18 -1
- package/steering/design-html/skills/pptx-style-extract/scripts/package.py +392 -14
- package/steering/design-html/skills/pptx-style-extract/scripts/parts.py +3 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/query.py +3 -8
- package/steering/design-html/skills/pptx-style-extract/scripts/test_asset_judgment_package.py +161 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_background_composite.py +57 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_color_contract.py +60 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_design_consumer_contract.py +63 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_flow_layout_contract.py +468 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_layout_css.py +503 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_rounded_contract.py +112 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_text_role_contract.py +208 -0
- package/steering/design-html/skills/pptx-style-extract/v2-format-spec.md +16 -7
- package/steering/design-html/skills/slide-deck/SKILL.md +15 -20
- package/steering/design-html/skills/slide-deck/scripts/check_local_references.py +179 -0
- package/steering/nestjs-react-fullstack/skills/plugin-guide/SKILL.md +5 -3
- package/steering/nestjs-react-fullstack/skills_local/plugin-guide/SKILL.md +4 -0
- package/steering/vite-react/skills/plugin-guide/SKILL.md +3 -1
- package/steering/vite-react/skills/react-three-fiber/SKILL.md +4 -0
|
@@ -11,6 +11,8 @@ as-is *and* re-encoded to a webp under ASSET_BUDGET_BYTES; without Pillow the
|
|
|
11
11
|
transcode falls back to darwin `sips`, and failing that the row records
|
|
12
12
|
transcode_blocked and extract.json records pillow_available: false.
|
|
13
13
|
"""
|
|
14
|
+
import hashlib
|
|
15
|
+
import io
|
|
14
16
|
import json
|
|
15
17
|
import os
|
|
16
18
|
import re
|
|
@@ -162,6 +164,7 @@ def _webp_from_sips(part, raw, dst_base, budget):
|
|
|
162
164
|
def export_media(pkg, images, outdir, pillow_ok, export_all=False):
|
|
163
165
|
media_dir = os.path.join(outdir, 'media-out')
|
|
164
166
|
os.makedirs(media_dir, exist_ok=True)
|
|
167
|
+
written = {}
|
|
165
168
|
by_media = {i['media']: i for i in images}
|
|
166
169
|
rows = []
|
|
167
170
|
for part in pkg.media:
|
|
@@ -196,7 +199,15 @@ def export_media(pkg, images, outdir, pillow_ok, export_all=False):
|
|
|
196
199
|
body = raw.decode('utf-8', 'replace')
|
|
197
200
|
row['embedded_raster'] = 'data:image/' in body
|
|
198
201
|
out_name = os.path.basename(part)
|
|
199
|
-
|
|
202
|
+
dst_path = os.path.join(media_dir, out_name)
|
|
203
|
+
if dst_path in written:
|
|
204
|
+
# 两个 media part 落到同一个输出名。不覆盖——覆盖等于悄悄换掉一张图。
|
|
205
|
+
row['export_name_conflict'] = written[dst_path]
|
|
206
|
+
out_name = '%s~%d%s' % (os.path.splitext(out_name)[0], len(written),
|
|
207
|
+
os.path.splitext(out_name)[1])
|
|
208
|
+
dst_path = os.path.join(media_dir, out_name)
|
|
209
|
+
written[dst_path] = part
|
|
210
|
+
with open(dst_path, 'wb') as f:
|
|
200
211
|
f.write(raw)
|
|
201
212
|
row.update({'exported': True, 'out': 'media-out/' + out_name,
|
|
202
213
|
'out_bytes': len(raw), 'transcoded': False,
|
|
@@ -215,13 +226,11 @@ def export_media(pkg, images, outdir, pillow_ok, export_all=False):
|
|
|
215
226
|
# rasterising it here would throw away the vector original.
|
|
216
227
|
if needs and ext != 'svg':
|
|
217
228
|
row['needs_transcode'] = needs
|
|
218
|
-
#
|
|
219
|
-
#
|
|
220
|
-
#
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
stem += '-min'
|
|
224
|
-
base = os.path.join(media_dir, stem)
|
|
229
|
+
# 压缩产物必须用**完整原名**当前缀:只去掉扩展名的话,image4.png 的压缩版
|
|
230
|
+
# 就叫 image4.webp —— 而 ppt/media/image4.webp 往往是另一张真实存在的图,
|
|
231
|
+
# 两者抢同一个输出名,后写的覆盖先写的,且全程无人报错。实测某模板因此把
|
|
232
|
+
# 一张 109x109 的小图标当成了整页背景。
|
|
233
|
+
base = os.path.join(media_dir, out_name + '-min')
|
|
225
234
|
done, why = None, 'pillow-unavailable' if not pillow_ok else None
|
|
226
235
|
if pillow_ok:
|
|
227
236
|
try:
|
|
@@ -244,6 +253,214 @@ def export_media(pkg, images, outdir, pillow_ok, export_all=False):
|
|
|
244
253
|
return rows
|
|
245
254
|
|
|
246
255
|
|
|
256
|
+
# ---------------------------------------------------- rendered backgrounds
|
|
257
|
+
def _is_full_canvas_picture(shape):
|
|
258
|
+
return (shape.get('kind') == 'pic' and shape.get('media')
|
|
259
|
+
and not shape.get('hidden')
|
|
260
|
+
and (shape.get('w_pct') or 0) >= 95
|
|
261
|
+
and (shape.get('h_pct') or 0) >= 95)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _picture_has_appearance(shape):
|
|
265
|
+
return bool(shape.get('flipH') or shape.get('flipV') or shape.get('rot')
|
|
266
|
+
or shape.get('crop')
|
|
267
|
+
or (shape.get('opacity') is not None and shape.get('opacity') != 1.0))
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _effective_background(parts, bg_by_part):
|
|
271
|
+
background = None
|
|
272
|
+
for part in parts:
|
|
273
|
+
if bg_by_part.get(part):
|
|
274
|
+
background = bg_by_part[part]
|
|
275
|
+
return background
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _draw_picture(canvas, shape, pkg, image_cache=None):
|
|
279
|
+
"""Replay one p:pic's crop, flip, rotation and blip opacity with Pillow."""
|
|
280
|
+
from PIL import Image, ImageEnhance, ImageOps
|
|
281
|
+
|
|
282
|
+
media = shape.get('media')
|
|
283
|
+
if media not in pkg.names:
|
|
284
|
+
return False
|
|
285
|
+
image_cache = image_cache if image_cache is not None else {}
|
|
286
|
+
box = shape.get('box_unrotated') or shape.get('box') or {}
|
|
287
|
+
if media not in image_cache:
|
|
288
|
+
try:
|
|
289
|
+
source = Image.open(io.BytesIO(pkg.zip.read(media))).convert('RGBA')
|
|
290
|
+
source.load()
|
|
291
|
+
image_cache[media] = source
|
|
292
|
+
except Exception:
|
|
293
|
+
return False
|
|
294
|
+
picture = image_cache[media].copy()
|
|
295
|
+
|
|
296
|
+
crop = shape.get('crop') or {}
|
|
297
|
+
left = max(0.0, min(float(crop.get('l', 0) or 0), 100.0))
|
|
298
|
+
top = max(0.0, min(float(crop.get('t', 0) or 0), 100.0))
|
|
299
|
+
right = max(0.0, min(float(crop.get('r', 0) or 0), 100.0))
|
|
300
|
+
bottom = max(0.0, min(float(crop.get('b', 0) or 0), 100.0))
|
|
301
|
+
bounds = (
|
|
302
|
+
int(round(picture.width * left / 100.0)),
|
|
303
|
+
int(round(picture.height * top / 100.0)),
|
|
304
|
+
int(round(picture.width * (1.0 - right / 100.0))),
|
|
305
|
+
int(round(picture.height * (1.0 - bottom / 100.0))),
|
|
306
|
+
)
|
|
307
|
+
if bounds[2] > bounds[0] and bounds[3] > bounds[1]:
|
|
308
|
+
picture = picture.crop(bounds)
|
|
309
|
+
|
|
310
|
+
width = max(1, int(round(box.get('w') or 0)))
|
|
311
|
+
height = max(1, int(round(box.get('h') or 0)))
|
|
312
|
+
resampling = getattr(Image, 'Resampling', Image)
|
|
313
|
+
picture = picture.resize((width, height), resampling.LANCZOS)
|
|
314
|
+
if shape.get('flipH'):
|
|
315
|
+
picture = ImageOps.mirror(picture)
|
|
316
|
+
if shape.get('flipV'):
|
|
317
|
+
picture = ImageOps.flip(picture)
|
|
318
|
+
opacity = shape.get('opacity')
|
|
319
|
+
if opacity is not None:
|
|
320
|
+
picture.putalpha(ImageEnhance.Brightness(picture.getchannel('A')).enhance(
|
|
321
|
+
max(0.0, min(float(opacity), 1.0))))
|
|
322
|
+
|
|
323
|
+
rotation = float(shape.get('rot') or 0)
|
|
324
|
+
if rotation:
|
|
325
|
+
picture = picture.rotate(-rotation, resample=resampling.BICUBIC, expand=True)
|
|
326
|
+
center_x = float(box.get('x') or 0) + width / 2.0
|
|
327
|
+
center_y = float(box.get('y') or 0) + height / 2.0
|
|
328
|
+
x = int(round(center_x - picture.width / 2.0))
|
|
329
|
+
y = int(round(center_y - picture.height / 2.0))
|
|
330
|
+
layer = Image.new('RGBA', canvas.size)
|
|
331
|
+
layer.alpha_composite(picture, (x, y))
|
|
332
|
+
canvas.alpha_composite(layer)
|
|
333
|
+
return True
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _draw_background(canvas, background, pkg, image_cache=None):
|
|
337
|
+
"""Draw the effective p:bg underneath picture layers."""
|
|
338
|
+
from PIL import Image
|
|
339
|
+
from render_pages import _grad_image, _grad_stops, _rgba, background_css
|
|
340
|
+
|
|
341
|
+
css = background_css(background)
|
|
342
|
+
canvas.paste(_rgba(css, (255, 255, 255, 255)),
|
|
343
|
+
(0, 0, canvas.width, canvas.height))
|
|
344
|
+
gradient = _grad_stops(css or '')
|
|
345
|
+
if gradient:
|
|
346
|
+
canvas.alpha_composite(
|
|
347
|
+
_grad_image(Image, canvas.width, canvas.height, gradient[0], gradient[1]))
|
|
348
|
+
if background and background.get('type') == 'image' and background.get('media'):
|
|
349
|
+
_draw_picture(canvas, {
|
|
350
|
+
'media': background['media'],
|
|
351
|
+
'box_unrotated': {'x': 0, 'y': 0, 'w': canvas.width, 'h': canvas.height},
|
|
352
|
+
'crop': background.get('crop') or {},
|
|
353
|
+
}, pkg, image_cache)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _save_background_webp(canvas, path):
|
|
357
|
+
"""Prefer lossless output; fall back to a quality ladder when oversized."""
|
|
358
|
+
canvas.convert('RGB').save(path, 'WEBP', lossless=True, method=0)
|
|
359
|
+
if os.path.getsize(path) <= ASSET_BUDGET_BYTES:
|
|
360
|
+
return
|
|
361
|
+
rgb = canvas.convert('RGB')
|
|
362
|
+
for quality in (95, 90, 85, 75):
|
|
363
|
+
rgb.save(path, 'WEBP', quality=quality, method=6)
|
|
364
|
+
if os.path.getsize(path) <= ASSET_BUDGET_BYTES:
|
|
365
|
+
return
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def compose_backgrounds(pkg, graph, shapes, bg_by_part, units, outdir, pillow_ok):
|
|
369
|
+
"""Flatten non-trivial full-canvas picture stacks into one web-safe asset."""
|
|
370
|
+
if not pillow_ok:
|
|
371
|
+
return {}, [], []
|
|
372
|
+
try:
|
|
373
|
+
from PIL import Image
|
|
374
|
+
except Exception:
|
|
375
|
+
return {}, [], []
|
|
376
|
+
|
|
377
|
+
by_part = defaultdict(list)
|
|
378
|
+
for shape in shapes:
|
|
379
|
+
by_part[shape['part']].append(shape)
|
|
380
|
+
|
|
381
|
+
chains = {}
|
|
382
|
+
for layout in pkg.layouts:
|
|
383
|
+
master = graph['master_of_layout'].get(layout)
|
|
384
|
+
show_master = pkg.xml(layout).get('showMasterSp', '1') != '0'
|
|
385
|
+
chains[layout] = [part for part in ((master if show_master else None), layout) if part]
|
|
386
|
+
for slide in pkg.slides:
|
|
387
|
+
layout = graph['layout_of_slide'].get(slide)
|
|
388
|
+
master = graph['master_of_layout'].get(layout)
|
|
389
|
+
show_master = pkg.xml(slide).get('showMasterSp', '1') != '0'
|
|
390
|
+
if layout:
|
|
391
|
+
show_master = show_master and pkg.xml(layout).get('showMasterSp', '1') != '0'
|
|
392
|
+
chains[slide] = [part for part in (
|
|
393
|
+
(master if show_master else None), layout, slide) if part]
|
|
394
|
+
|
|
395
|
+
media_dir = os.path.join(outdir, 'media-out')
|
|
396
|
+
os.makedirs(media_dir, exist_ok=True)
|
|
397
|
+
part_map, rows_by_media, images_by_media, rendered_specs = {}, {}, {}, {}
|
|
398
|
+
image_cache = {}
|
|
399
|
+
for target, chain in chains.items():
|
|
400
|
+
layers = [shape for part in chain for shape in by_part.get(part, [])
|
|
401
|
+
if _is_full_canvas_picture(shape)]
|
|
402
|
+
if not layers or (len(layers) == 1 and not _picture_has_appearance(layers[0])):
|
|
403
|
+
continue
|
|
404
|
+
source_layers = [{
|
|
405
|
+
'part': shape['part'], 'media': shape['media'],
|
|
406
|
+
'crop': shape.get('crop'), 'flipH': bool(shape.get('flipH')),
|
|
407
|
+
'flipV': bool(shape.get('flipV')), 'rot': shape.get('rot') or 0,
|
|
408
|
+
'opacity': shape.get('opacity', 1.0),
|
|
409
|
+
} for shape in layers]
|
|
410
|
+
spec = json.dumps({
|
|
411
|
+
'background': _effective_background(chain, bg_by_part),
|
|
412
|
+
'layers': [{k: v for k, v in layer.items() if k != 'part'}
|
|
413
|
+
for layer in source_layers],
|
|
414
|
+
}, ensure_ascii=False, sort_keys=True)
|
|
415
|
+
media = rendered_specs.get(spec)
|
|
416
|
+
if media is None:
|
|
417
|
+
canvas = Image.new('RGBA', (units.w, units.h))
|
|
418
|
+
_draw_background(
|
|
419
|
+
canvas, _effective_background(chain, bg_by_part), pkg, image_cache)
|
|
420
|
+
drawn = [shape for shape in layers
|
|
421
|
+
if _draw_picture(canvas, shape, pkg, image_cache)]
|
|
422
|
+
if len(drawn) != len(layers):
|
|
423
|
+
continue
|
|
424
|
+
digest = hashlib.sha256(canvas.tobytes()).hexdigest()[:16]
|
|
425
|
+
media = 'generated/background/bg-composite-%s.webp' % digest
|
|
426
|
+
out = 'media-out/' + os.path.basename(media)
|
|
427
|
+
path = os.path.join(outdir, out)
|
|
428
|
+
if not os.path.exists(path):
|
|
429
|
+
_save_background_webp(canvas, path)
|
|
430
|
+
rendered_specs[spec] = media
|
|
431
|
+
else:
|
|
432
|
+
out = rows_by_media[media]['out']
|
|
433
|
+
path = os.path.join(outdir, out)
|
|
434
|
+
part_map[target] = media
|
|
435
|
+
row = rows_by_media.setdefault(media, {
|
|
436
|
+
'media': media, 'ext': 'webp', 'bytes': os.path.getsize(path),
|
|
437
|
+
'used_n': 0, 'reasons': ['background_composite'], 'candidate': True,
|
|
438
|
+
'exported': True, 'out': out, 'out_bytes': os.path.getsize(path),
|
|
439
|
+
'transcoded': False, 'generated': True, 'composited_from': source_layers,
|
|
440
|
+
'part_refs': [],
|
|
441
|
+
})
|
|
442
|
+
row['used_n'] += 1
|
|
443
|
+
row['part_refs'].append(target)
|
|
444
|
+
image = images_by_media.setdefault(media, {
|
|
445
|
+
'media': media, 'n': 0, 'boxes': [], 'exact_boxes': [],
|
|
446
|
+
'fullscreen': True, 'fullscreen_n': 0, 'fullscreen_top_cluster_n': 0,
|
|
447
|
+
'repeat_fixed': [], 'max_w_pct': 100.0, 'bleed': False,
|
|
448
|
+
'crop_variants': [], 'stitch_candidate': False,
|
|
449
|
+
'svg_companion': None, 'variant_group': [],
|
|
450
|
+
})
|
|
451
|
+
image['n'] += 1
|
|
452
|
+
image['fullscreen_n'] += 1
|
|
453
|
+
image['fullscreen_top_cluster_n'] += 1
|
|
454
|
+
image['boxes'].append({
|
|
455
|
+
'box': {'x': 0, 'y': 0, 'w': units.w, 'h': units.h},
|
|
456
|
+
'box_emu': {'x': 0, 'y': 0, 'cx': units.cx, 'cy': units.cy},
|
|
457
|
+
'count': 1, 'exact_count': 1, 'exact_variants': 1,
|
|
458
|
+
'w_pct': 100.0, 'h_pct': 100.0, 'parts': [target],
|
|
459
|
+
'layers': ['composite'],
|
|
460
|
+
})
|
|
461
|
+
return part_map, list(rows_by_media.values()), list(images_by_media.values())
|
|
462
|
+
|
|
463
|
+
|
|
247
464
|
# ---------------------------------------------------------------- form hint
|
|
248
465
|
# PowerPoint 出厂版式名(中英两套)。设计师起的名字是「这一页干什么用」,
|
|
249
466
|
# 出厂名只是「这个占位符组合叫什么」——后者不算模板声明了页型。
|
|
@@ -466,6 +683,30 @@ REF_NOTES = """# ref/ 审计层说明(S12)
|
|
|
466
683
|
"""
|
|
467
684
|
|
|
468
685
|
|
|
686
|
+
def dump_source(pkg, outdir):
|
|
687
|
+
"""把 PPTX 解压后的原文原样落到 ref/source/。
|
|
688
|
+
|
|
689
|
+
普查是有损的:它按既定口径抽数,抽不到的、口径外的东西就没了。遇到判断不了的
|
|
690
|
+
情况(这个形状为什么这么摆、某个字段是什么意思),能直接翻原始 XML 比对着二手
|
|
691
|
+
数据猜可靠得多。只进中间产物,交付包里没有。
|
|
692
|
+
"""
|
|
693
|
+
dst = os.path.abspath(os.path.join(outdir, 'ref', 'source'))
|
|
694
|
+
n = 0
|
|
695
|
+
for name in pkg.zip.namelist():
|
|
696
|
+
if name.endswith('/'):
|
|
697
|
+
continue
|
|
698
|
+
p = os.path.abspath(os.path.join(dst, *name.split('/')))
|
|
699
|
+
# zip 条目名是文件里写什么就是什么,带 ../ 就能写到 ref/source 外面去
|
|
700
|
+
# (pptx 是用户上传的,当不可信输入处理)。落在目录外的条目一律不落盘。
|
|
701
|
+
if not p.startswith(dst + os.sep):
|
|
702
|
+
continue
|
|
703
|
+
os.makedirs(os.path.dirname(p), exist_ok=True)
|
|
704
|
+
with open(p, 'wb') as f:
|
|
705
|
+
f.write(pkg.zip.read(name))
|
|
706
|
+
n += 1
|
|
707
|
+
return n
|
|
708
|
+
|
|
709
|
+
|
|
469
710
|
def write_ref(outdir, payload, ref_data, units, parts_ordered, shapes, bg_by_part):
|
|
470
711
|
ref = os.path.join(outdir, 'ref')
|
|
471
712
|
os.makedirs(ref, exist_ok=True)
|
|
@@ -604,11 +845,16 @@ def extract(pptx, outdir, export_all=False):
|
|
|
604
845
|
t = mark('derived_censuses', t)
|
|
605
846
|
|
|
606
847
|
media_rows = export_media(pkg, images, outdir, pillow_ok, export_all) # S9
|
|
848
|
+
background_composites, composite_media, composite_images = compose_backgrounds(
|
|
849
|
+
pkg, graph, shapes, bg_by_part, units, outdir, pillow_ok)
|
|
850
|
+
media_rows += composite_media
|
|
851
|
+
images += composite_images
|
|
607
852
|
t = mark('S9_media_export', t)
|
|
608
853
|
|
|
609
854
|
# S5b + S14 run after S9 because the palette only covers exported assets.
|
|
610
|
-
|
|
611
|
-
|
|
855
|
+
source_images = [i for i in images if i['media'] in pkg.names]
|
|
856
|
+
fps = media_fingerprints(pkg, [i['media'] for i in source_images], pillow_ok)
|
|
857
|
+
clusters, cluster_evidence = content_clusters(source_images, fps)
|
|
612
858
|
exported = {m['media'] for m in media_rows if m.get('exported')}
|
|
613
859
|
image_palette(images, fps, exported)
|
|
614
860
|
t = mark('S5b_S14_content_palette', t)
|
|
@@ -680,6 +926,7 @@ def extract(pptx, outdir, export_all=False):
|
|
|
680
926
|
'Pillow unavailable: byte-identical media only, no perceptual merging'),
|
|
681
927
|
'palette_available': pillow_ok,
|
|
682
928
|
'media': media_rows,
|
|
929
|
+
'background_composites': background_composites,
|
|
683
930
|
# S4 shape-facts dominate this file (roughly half its bytes) and stage 2 reads
|
|
684
931
|
# them only when a derived statistic needs backing evidence, so they live in
|
|
685
932
|
# a sidecar and extract.json keeps just the pointer plus the derived censuses.
|
|
@@ -690,6 +937,7 @@ def extract(pptx, outdir, export_all=False):
|
|
|
690
937
|
'slides': len(pkg.slides), 'layouts': len(pkg.layouts),
|
|
691
938
|
'masters': len(pkg.masters), 'themes': len(pkg.themes),
|
|
692
939
|
'media': len(pkg.media),
|
|
940
|
+
'background_composites': len(composite_media),
|
|
693
941
|
'shapes_total': len(shapes),
|
|
694
942
|
'shapes_kept': len(kept_shapes),
|
|
695
943
|
'shapes_dropped_off_canvas': len(dropped_shapes),
|
|
@@ -699,8 +947,10 @@ def extract(pptx, outdir, export_all=False):
|
|
|
699
947
|
if r.get('placement') == 'inherited'),
|
|
700
948
|
'content_clusters': len(clusters),
|
|
701
949
|
'content_clusters_multi_media': sum(1 for c in clusters if c['member_n'] > 1),
|
|
702
|
-
'media_exported': sum(1 for m in media_rows
|
|
703
|
-
|
|
950
|
+
'media_exported': sum(1 for m in media_rows
|
|
951
|
+
if m.get('exported') and not m.get('generated')),
|
|
952
|
+
'media_transcoded': sum(1 for m in media_rows
|
|
953
|
+
if m.get('transcoded') and not m.get('generated')),
|
|
704
954
|
'media_transcode_blocked': sum(1 for m in media_rows
|
|
705
955
|
if m.get('transcode_blocked')),
|
|
706
956
|
'media_over_budget': sum(1 for m in media_rows if m.get('over_budget')),
|
|
@@ -757,6 +1007,7 @@ def extract(pptx, outdir, export_all=False):
|
|
|
757
1007
|
with open(out_json, 'w') as f:
|
|
758
1008
|
json.dump(payload, f, ensure_ascii=False, indent=1)
|
|
759
1009
|
write_ref(outdir, payload, ref_data, units, parts_ordered, shapes, bg_by_part)
|
|
1010
|
+
src_n = dump_source(pkg, outdir)
|
|
760
1011
|
perf['total'] = round(time.time() - t0, 3)
|
|
761
1012
|
with open(os.path.join(outdir, 'ref', 'perf.json'), 'w') as f:
|
|
762
1013
|
json.dump(perf, f, ensure_ascii=False, indent=1)
|
|
@@ -768,9 +1019,11 @@ def extract(pptx, outdir, export_all=False):
|
|
|
768
1019
|
print(' shapes %d kept / %d dropped off-canvas / %d bleed / %d clamped'
|
|
769
1020
|
% (payload['counts']['shapes_kept'], payload['counts']['shapes_dropped_off_canvas'],
|
|
770
1021
|
payload['counts']['shapes_bleed'], payload['counts']['shapes_clamped']))
|
|
771
|
-
print(' colors %d fonts %d images %d media exported %d/%d guides %d'
|
|
1022
|
+
print(' colors %d fonts %d images %d media exported %d/%d + %d composites guides %d'
|
|
772
1023
|
% (len(color_freq), len(fonts), len(images),
|
|
773
|
-
payload['counts']['media_exported'], len(pkg.media),
|
|
1024
|
+
payload['counts']['media_exported'], len(pkg.media),
|
|
1025
|
+
len(composite_media), len(guides)))
|
|
1026
|
+
print(' ref/source/ 原文 %d 个部件(判断不了时可直接翻)' % src_n)
|
|
774
1027
|
print(' extract.json %.1f KB total %.2fs'
|
|
775
1028
|
% (os.path.getsize(out_json) / 1024.0, perf['total']))
|
|
776
1029
|
return payload
|
|
@@ -796,9 +1049,18 @@ def main(argv):
|
|
|
796
1049
|
if '--no-draft' not in flags:
|
|
797
1050
|
import subprocess
|
|
798
1051
|
d = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'draft.py')
|
|
1052
|
+
sys.stdout.flush()
|
|
799
1053
|
r = subprocess.run([sys.executable, d, args[1]])
|
|
800
1054
|
if r.returncode:
|
|
801
|
-
|
|
1055
|
+
# 普查产物已经齐了,缺的只是草案。重跑整条抽取会同样失败在这一步,
|
|
1056
|
+
# 所以给一个区别于成功的终止哨兵,并指明只需重跑 draft.py。
|
|
1057
|
+
print('EXTRACT_PARTIAL 普查产物齐全,草案生成失败:'
|
|
1058
|
+
'python3 -B scripts/draft.py %s 单独重跑看报错' % args[1])
|
|
1059
|
+
sys.stdout.flush()
|
|
1060
|
+
return 1
|
|
1061
|
+
# 最后一行是终止哨兵:stdout 被截断时退出码仍可能是 0,两个哨兵都没有就是没跑完。
|
|
1062
|
+
print('EXTRACT_OK %s' % os.path.join(args[1], 'l-out'))
|
|
1063
|
+
sys.stdout.flush()
|
|
802
1064
|
return 0
|
|
803
1065
|
|
|
804
1066
|
|
|
@@ -519,8 +519,25 @@ def read_txbody(tx, ctx, kind='txBody'):
|
|
|
519
519
|
ins[k] = ctx.units.px(bp.get(k))
|
|
520
520
|
if ins:
|
|
521
521
|
b['insets_px'] = ins
|
|
522
|
-
|
|
522
|
+
na = bp.find('a:normAutofit', NS)
|
|
523
|
+
if na is not None:
|
|
523
524
|
b['autofit'] = 'norm'
|
|
525
|
+
# normAutofit 用 fontScale / lnSpcReduction(单位 1/1000 %)把大字缩进小框——
|
|
526
|
+
# 章节大号数字就靠它让 160px 的字装进 144px 的框。只记 autofit 存在、丢掉
|
|
527
|
+
# fontScale,消费端就拿到未缩放字号 + 原始框高,字比框高,渐变裁切把溢出的
|
|
528
|
+
# 底部切成透明。缺省即 100%(无缩放)。
|
|
529
|
+
fs = na.get('fontScale')
|
|
530
|
+
if fs is not None:
|
|
531
|
+
try:
|
|
532
|
+
b['font_scale'] = round(int(fs) / 100000.0, 4)
|
|
533
|
+
except (TypeError, ValueError):
|
|
534
|
+
pass
|
|
535
|
+
lsr = na.get('lnSpcReduction')
|
|
536
|
+
if lsr is not None:
|
|
537
|
+
try:
|
|
538
|
+
b['ln_spc_reduction'] = round(int(lsr) / 100000.0, 4)
|
|
539
|
+
except (TypeError, ValueError):
|
|
540
|
+
pass
|
|
524
541
|
elif bp.find('a:spAutoFit', NS) is not None:
|
|
525
542
|
b['autofit'] = 'shape'
|
|
526
543
|
if b:
|