@lark-apaas/coding-steering 0.1.38 → 0.1.39-alpha.20260824122813
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 +6 -6
- package/steering/design-html/skills/pptx-style-extract/SKILL.md +19 -28
- package/steering/design-html/skills/pptx-style-extract/scripts/census.py +2 -8
- package/steering/design-html/skills/pptx-style-extract/scripts/draft.py +190 -1181
- package/steering/design-html/skills/pptx-style-extract/scripts/extract.py +10 -229
- package/steering/design-html/skills/pptx-style-extract/scripts/ooxml.py +1 -18
- package/steering/design-html/skills/pptx-style-extract/scripts/package.py +40 -643
- package/steering/design-html/skills/pptx-style-extract/scripts/parts.py +3 -19
- package/steering/design-html/skills/pptx-style-extract/scripts/render_pages.py +2 -4
- package/steering/design-html/skills/pptx-style-extract/scripts/test_background_composite.py +1 -308
- package/steering/design-html/skills/pptx-style-extract/scripts/test_design_consumer_contract.py +0 -14
- package/steering/design-html/skills/pptx-style-extract/scripts/test_flow_layout_contract.py +7 -157
- package/steering/design-html/skills/pptx-style-extract/scripts/test_layout_css.py +2 -1598
- package/steering/design-html/skills/pptx-style-extract/scripts/test_text_role_contract.py +4 -151
- package/steering/design-html/skills/pptx-style-extract/v2-format-spec.md +1 -1
- package/steering/design-html/skills/preflight/scripts/probe.sh +0 -0
- package/steering/nestjs-react-fullstack/skills/coding-guide/SKILL.md +0 -11
- package/steering/nestjs-react-fullstack/skills/contacts-service/SKILL.md +4 -67
- package/steering/nestjs-react-fullstack/skills/nestjs-cache/SKILL.md +1 -0
- package/steering/nestjs-react-fullstack/skills/openapi-guide/SKILL.md +0 -75
- package/steering/nestjs-react-fullstack/skills_local/coding-guide/SKILL.md +7 -5
- package/steering/design-html/skills/pptx-style-extract/scripts/test_asset_judgment_package.py +0 -556
- package/steering/design-html/skills/pptx-style-extract/scripts/test_logo_scope.py +0 -479
- package/steering/design-html/skills/pptx-style-extract/scripts/verify_layout_assets.py +0 -400
- package/steering/design-html/skills/pptx-style-extract/scripts/verify_logo_scope.py +0 -12
|
@@ -181,8 +181,6 @@ def export_media(pkg, images, outdir, pillow_ok, export_all=False):
|
|
|
181
181
|
reasons.append('variant_group')
|
|
182
182
|
if info['stitch_candidate']:
|
|
183
183
|
reasons.append('crop_stitch')
|
|
184
|
-
if info.get('visible_instance_blocked'):
|
|
185
|
-
reasons.append('visible_instance_fallback')
|
|
186
184
|
if 0 < (info.get('max_w_pct') or 0) < SMALL_IMG_W_PCT and not info['fullscreen']:
|
|
187
185
|
# 小图(页内图标、角标)。不导出的话 L 层只能看着装饰容器里的空洞
|
|
188
186
|
# 自己编图形,编出来的与模板无关。
|
|
@@ -263,30 +261,6 @@ def _is_full_canvas_picture(shape):
|
|
|
263
261
|
and (shape.get('h_pct') or 0) >= 95)
|
|
264
262
|
|
|
265
263
|
|
|
266
|
-
def _shape_has_visible_text(shape):
|
|
267
|
-
text = shape.get('text') or {}
|
|
268
|
-
for paragraph in text.get('paragraphs') or []:
|
|
269
|
-
for run in paragraph.get('runs') or []:
|
|
270
|
-
if str(run.get('text') or '').strip():
|
|
271
|
-
return True
|
|
272
|
-
return False
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
def _is_full_canvas_fill_overlay(shape):
|
|
276
|
-
fill = shape.get('fill') or {}
|
|
277
|
-
geometry = shape.get('geom') or {}
|
|
278
|
-
line = shape.get('line') or {}
|
|
279
|
-
return (shape.get('kind') == 'sp' and not shape.get('hidden')
|
|
280
|
-
and fill.get('type') in ('solid', 'gradient')
|
|
281
|
-
and not fill.get('path')
|
|
282
|
-
and geometry.get('prst') == 'rect'
|
|
283
|
-
and not shape.get('rot')
|
|
284
|
-
and not line.get('color') and not line.get('gradient')
|
|
285
|
-
and not _shape_has_visible_text(shape)
|
|
286
|
-
and (shape.get('w_pct') or 0) >= 95
|
|
287
|
-
and (shape.get('h_pct') or 0) >= 95)
|
|
288
|
-
|
|
289
|
-
|
|
290
264
|
def _picture_has_appearance(shape):
|
|
291
265
|
return bool(shape.get('flipH') or shape.get('flipV') or shape.get('rot')
|
|
292
266
|
or shape.get('crop')
|
|
@@ -359,147 +333,6 @@ def _draw_picture(canvas, shape, pkg, image_cache=None):
|
|
|
359
333
|
return True
|
|
360
334
|
|
|
361
335
|
|
|
362
|
-
def _visible_picture_instance(shape, pkg, image_cache=None):
|
|
363
|
-
"""Render one non-background picture exactly as its PPT instance appears."""
|
|
364
|
-
from PIL import Image
|
|
365
|
-
|
|
366
|
-
bounds = shape.get('box') or {}
|
|
367
|
-
source_box = shape.get('box_unrotated') or bounds
|
|
368
|
-
width = max(1, int(round(bounds.get('w') or 0)))
|
|
369
|
-
height = max(1, int(round(bounds.get('h') or 0)))
|
|
370
|
-
if not source_box.get('w') or not source_box.get('h'):
|
|
371
|
-
return None
|
|
372
|
-
local = dict(shape)
|
|
373
|
-
local['box_unrotated'] = {
|
|
374
|
-
'x': float(source_box.get('x') or 0) - float(bounds.get('x') or 0),
|
|
375
|
-
'y': float(source_box.get('y') or 0) - float(bounds.get('y') or 0),
|
|
376
|
-
'w': source_box['w'],
|
|
377
|
-
'h': source_box['h'],
|
|
378
|
-
}
|
|
379
|
-
canvas = Image.new('RGBA', (width, height))
|
|
380
|
-
if not _draw_picture(canvas, local, pkg, image_cache):
|
|
381
|
-
return None
|
|
382
|
-
return canvas if canvas.getchannel('A').getbbox() else None
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
def _save_picture_instance_webp(canvas, path):
|
|
386
|
-
"""Keep the instance alpha and shrink only after lossless output exceeds budget."""
|
|
387
|
-
def write(options):
|
|
388
|
-
temporary = path + '.tmp'
|
|
389
|
-
try:
|
|
390
|
-
canvas.save(temporary, 'WEBP', **options)
|
|
391
|
-
size = os.path.getsize(temporary)
|
|
392
|
-
os.replace(temporary, path)
|
|
393
|
-
return size
|
|
394
|
-
finally:
|
|
395
|
-
if os.path.exists(temporary):
|
|
396
|
-
os.unlink(temporary)
|
|
397
|
-
|
|
398
|
-
try:
|
|
399
|
-
size = write({'lossless': True, 'method': 0})
|
|
400
|
-
except Exception as exc:
|
|
401
|
-
return 'pillow-error: %s: %s' % (exc.__class__.__name__, exc)
|
|
402
|
-
if size <= ASSET_BUDGET_BYTES:
|
|
403
|
-
return None
|
|
404
|
-
for quality in (95, 90, 85, 75):
|
|
405
|
-
try:
|
|
406
|
-
size = write({'quality': quality, 'method': 6})
|
|
407
|
-
except Exception:
|
|
408
|
-
return None
|
|
409
|
-
if size <= ASSET_BUDGET_BYTES:
|
|
410
|
-
return None
|
|
411
|
-
return None
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
def export_visible_picture_instances(pkg, shapes, outdir, pillow_ok, blocked=None):
|
|
415
|
-
"""Replace transformed local pictures with rendered media consumers can reuse.
|
|
416
|
-
|
|
417
|
-
A raw media file cannot reproduce an instance-level crop, rotation, flip, or
|
|
418
|
-
opacity. Flattening only those non-fullscreen instances keeps the downstream
|
|
419
|
-
contract simple: every asset still resolves through ordinary ``source_media``.
|
|
420
|
-
"""
|
|
421
|
-
if not pillow_ok:
|
|
422
|
-
return []
|
|
423
|
-
try:
|
|
424
|
-
from PIL import Image # noqa: F401
|
|
425
|
-
except Exception:
|
|
426
|
-
return []
|
|
427
|
-
|
|
428
|
-
media_dir = os.path.join(outdir, 'media-out')
|
|
429
|
-
os.makedirs(media_dir, exist_ok=True)
|
|
430
|
-
blocked = blocked if blocked is not None else {}
|
|
431
|
-
rows_by_media, rendered_specs, image_cache = {}, {}, {}
|
|
432
|
-
for shape in shapes:
|
|
433
|
-
if (shape.get('kind') != 'pic' or shape.get('hidden')
|
|
434
|
-
or not shape.get('media') or shape.get('media') not in pkg.names
|
|
435
|
-
or _is_full_canvas_picture(shape) or not _picture_has_appearance(shape)):
|
|
436
|
-
continue
|
|
437
|
-
source = shape['media']
|
|
438
|
-
source_box = shape.get('box_unrotated') or shape.get('box') or {}
|
|
439
|
-
visible_box = shape.get('box') or {}
|
|
440
|
-
spec = json.dumps({
|
|
441
|
-
'source': source,
|
|
442
|
-
'source_size': [source_box.get('w'), source_box.get('h')],
|
|
443
|
-
'visible_size': [visible_box.get('w'), visible_box.get('h')],
|
|
444
|
-
'crop': shape.get('crop') or {},
|
|
445
|
-
'flipH': bool(shape.get('flipH')),
|
|
446
|
-
'flipV': bool(shape.get('flipV')),
|
|
447
|
-
'rot': shape.get('rot') or 0,
|
|
448
|
-
'opacity': shape.get('opacity', 1.0),
|
|
449
|
-
}, ensure_ascii=False, sort_keys=True)
|
|
450
|
-
media = rendered_specs.get(spec)
|
|
451
|
-
if media is None:
|
|
452
|
-
canvas = _visible_picture_instance(shape, pkg, image_cache)
|
|
453
|
-
if canvas is None:
|
|
454
|
-
continue
|
|
455
|
-
digest = hashlib.sha256(spec.encode('utf-8')).hexdigest()[:16]
|
|
456
|
-
media = 'generated/instance/picture-%s.webp' % digest
|
|
457
|
-
out = 'media-out/' + os.path.basename(media)
|
|
458
|
-
path = os.path.join(outdir, out)
|
|
459
|
-
if not os.path.exists(path):
|
|
460
|
-
reason = _save_picture_instance_webp(canvas, path)
|
|
461
|
-
if reason:
|
|
462
|
-
row = blocked.setdefault(source, {
|
|
463
|
-
'reason': reason, 'attempted_specs': 0, 'part_refs': [],
|
|
464
|
-
})
|
|
465
|
-
row['attempted_specs'] += 1
|
|
466
|
-
row['part_refs'].append(shape.get('part'))
|
|
467
|
-
shape['visible_instance_blocked'] = reason
|
|
468
|
-
rendered_specs[spec] = False
|
|
469
|
-
continue
|
|
470
|
-
rendered_specs[spec] = media
|
|
471
|
-
rows_by_media[media] = {
|
|
472
|
-
'media': media,
|
|
473
|
-
'ext': 'webp',
|
|
474
|
-
'bytes': os.path.getsize(path),
|
|
475
|
-
'used_n': 0,
|
|
476
|
-
'reasons': ['visible_picture_instance'],
|
|
477
|
-
'candidate': True,
|
|
478
|
-
'exported': True,
|
|
479
|
-
'out': out,
|
|
480
|
-
'out_bytes': os.path.getsize(path),
|
|
481
|
-
'transcoded': False,
|
|
482
|
-
'generated': True,
|
|
483
|
-
'rendered_from': source,
|
|
484
|
-
'rendered_with': {
|
|
485
|
-
key: shape[key] for key in ('crop', 'flipH', 'flipV', 'rot', 'opacity')
|
|
486
|
-
if shape.get(key) not in (None, False, {}, 0, 1.0)
|
|
487
|
-
},
|
|
488
|
-
'part_refs': [],
|
|
489
|
-
}
|
|
490
|
-
elif media is False:
|
|
491
|
-
blocked[source]['part_refs'].append(shape.get('part'))
|
|
492
|
-
shape['visible_instance_blocked'] = blocked[source]['reason']
|
|
493
|
-
continue
|
|
494
|
-
row = rows_by_media[media]
|
|
495
|
-
row['used_n'] += 1
|
|
496
|
-
row['part_refs'].append(shape.get('part'))
|
|
497
|
-
shape['media'] = media
|
|
498
|
-
shape.pop('media_svg', None)
|
|
499
|
-
shape['rendered_from'] = source
|
|
500
|
-
return list(rows_by_media.values())
|
|
501
|
-
|
|
502
|
-
|
|
503
336
|
def _draw_background(canvas, background, pkg, image_cache=None):
|
|
504
337
|
"""Draw the effective p:bg underneath picture layers."""
|
|
505
338
|
from PIL import Image
|
|
@@ -520,48 +353,6 @@ def _draw_background(canvas, background, pkg, image_cache=None):
|
|
|
520
353
|
}, pkg, image_cache)
|
|
521
354
|
|
|
522
355
|
|
|
523
|
-
def _draw_background_fill_overlay(canvas, shape):
|
|
524
|
-
"""Draw one full-canvas fill shape that visually modifies a background image."""
|
|
525
|
-
from PIL import Image
|
|
526
|
-
from render_pages import _grad_image, _grad_stops, _rgba, css_color, css_gradient
|
|
527
|
-
|
|
528
|
-
fill = shape.get('fill') or {}
|
|
529
|
-
box = shape.get('box_unrotated') or shape.get('box') or {}
|
|
530
|
-
width = max(1, int(round(box.get('w') or 0)))
|
|
531
|
-
height = max(1, int(round(box.get('h') or 0)))
|
|
532
|
-
x = int(round(box.get('x') or 0))
|
|
533
|
-
y = int(round(box.get('y') or 0))
|
|
534
|
-
if fill.get('type') == 'solid':
|
|
535
|
-
color = _rgba(css_color(fill.get('color')))
|
|
536
|
-
if color is None:
|
|
537
|
-
return False
|
|
538
|
-
overlay = Image.new('RGBA', (width, height), color)
|
|
539
|
-
elif fill.get('type') == 'gradient':
|
|
540
|
-
gradient = _grad_stops(css_gradient(fill) or '')
|
|
541
|
-
if not gradient:
|
|
542
|
-
return False
|
|
543
|
-
overlay = _grad_image(Image, width, height, gradient[0], gradient[1])
|
|
544
|
-
else:
|
|
545
|
-
return False
|
|
546
|
-
canvas.alpha_composite(overlay, (x, y))
|
|
547
|
-
return True
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
def _background_layer_source(shape):
|
|
551
|
-
if shape.get('kind') == 'pic':
|
|
552
|
-
return {
|
|
553
|
-
'part': shape['part'], 'kind': 'picture', 'media': shape['media'],
|
|
554
|
-
'crop': shape.get('crop'), 'flipH': bool(shape.get('flipH')),
|
|
555
|
-
'flipV': bool(shape.get('flipV')), 'rot': shape.get('rot') or 0,
|
|
556
|
-
'opacity': shape.get('opacity', 1.0),
|
|
557
|
-
}
|
|
558
|
-
return {
|
|
559
|
-
'part': shape['part'], 'kind': 'fill_overlay',
|
|
560
|
-
'box': shape.get('box_unrotated') or shape.get('box'),
|
|
561
|
-
'fill': shape.get('fill'),
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
|
|
565
356
|
def _save_background_webp(canvas, path):
|
|
566
357
|
"""Prefer lossless output; fall back to a quality ladder when oversized."""
|
|
567
358
|
canvas.convert('RGB').save(path, 'WEBP', lossless=True, method=0)
|
|
@@ -607,12 +398,15 @@ def compose_backgrounds(pkg, graph, shapes, bg_by_part, units, outdir, pillow_ok
|
|
|
607
398
|
image_cache = {}
|
|
608
399
|
for target, chain in chains.items():
|
|
609
400
|
layers = [shape for part in chain for shape in by_part.get(part, [])
|
|
610
|
-
if _is_full_canvas_picture(shape)
|
|
611
|
-
|
|
612
|
-
if not pictures or (len(pictures) == 1 and not _picture_has_appearance(pictures[0])
|
|
613
|
-
and len(layers) == 1):
|
|
401
|
+
if _is_full_canvas_picture(shape)]
|
|
402
|
+
if not layers or (len(layers) == 1 and not _picture_has_appearance(layers[0])):
|
|
614
403
|
continue
|
|
615
|
-
source_layers = [
|
|
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]
|
|
616
410
|
spec = json.dumps({
|
|
617
411
|
'background': _effective_background(chain, bg_by_part),
|
|
618
412
|
'layers': [{k: v for k, v in layer.items() if k != 'part'}
|
|
@@ -623,12 +417,8 @@ def compose_backgrounds(pkg, graph, shapes, bg_by_part, units, outdir, pillow_ok
|
|
|
623
417
|
canvas = Image.new('RGBA', (units.w, units.h))
|
|
624
418
|
_draw_background(
|
|
625
419
|
canvas, _effective_background(chain, bg_by_part), pkg, image_cache)
|
|
626
|
-
drawn = [
|
|
627
|
-
|
|
628
|
-
if (_draw_picture(canvas, shape, pkg, image_cache)
|
|
629
|
-
if shape.get('kind') == 'pic'
|
|
630
|
-
else _draw_background_fill_overlay(canvas, shape))
|
|
631
|
-
]
|
|
420
|
+
drawn = [shape for shape in layers
|
|
421
|
+
if _draw_picture(canvas, shape, pkg, image_cache)]
|
|
632
422
|
if len(drawn) != len(layers):
|
|
633
423
|
continue
|
|
634
424
|
digest = hashlib.sha256(canvas.tobytes()).hexdigest()[:16]
|
|
@@ -1036,9 +826,6 @@ def extract(pptx, outdir, export_all=False):
|
|
|
1036
826
|
'box_emu': {'x': 0, 'y': 0, 'cx': cx, 'cy': cy},
|
|
1037
827
|
'crop': bg.get('crop'), 'placement': 'inside',
|
|
1038
828
|
'w_pct': 100.0, 'h_pct': 100.0, 'in_group': False})
|
|
1039
|
-
instance_blocked = {}
|
|
1040
|
-
instance_media = export_visible_picture_instances(
|
|
1041
|
-
pkg, shapes, outdir, pillow_ok, instance_blocked)
|
|
1042
829
|
images, variant_groups = image_census(shapes, bg_images, units) # S5
|
|
1043
830
|
t = mark('S5_image_census', t)
|
|
1044
831
|
|
|
@@ -1058,10 +845,6 @@ def extract(pptx, outdir, export_all=False):
|
|
|
1058
845
|
t = mark('derived_censuses', t)
|
|
1059
846
|
|
|
1060
847
|
media_rows = export_media(pkg, images, outdir, pillow_ok, export_all) # S9
|
|
1061
|
-
for row in media_rows:
|
|
1062
|
-
if row['media'] in instance_blocked:
|
|
1063
|
-
row['visible_instance_blocked'] = instance_blocked[row['media']]
|
|
1064
|
-
media_rows += instance_media
|
|
1065
848
|
background_composites, composite_media, composite_images = compose_backgrounds(
|
|
1066
849
|
pkg, graph, shapes, bg_by_part, units, outdir, pillow_ok)
|
|
1067
850
|
media_rows += composite_media
|
|
@@ -1166,8 +949,6 @@ def extract(pptx, outdir, export_all=False):
|
|
|
1166
949
|
'content_clusters_multi_media': sum(1 for c in clusters if c['member_n'] > 1),
|
|
1167
950
|
'media_exported': sum(1 for m in media_rows
|
|
1168
951
|
if m.get('exported') and not m.get('generated')),
|
|
1169
|
-
'media_instances': len(instance_media),
|
|
1170
|
-
'media_instance_blocked': len(instance_blocked),
|
|
1171
952
|
'media_transcoded': sum(1 for m in media_rows
|
|
1172
953
|
if m.get('transcoded') and not m.get('generated')),
|
|
1173
954
|
'media_transcode_blocked': sum(1 for m in media_rows
|
|
@@ -519,25 +519,8 @@ 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
|
-
|
|
523
|
-
if na is not None:
|
|
522
|
+
if bp.find('a:normAutofit', NS) is not None:
|
|
524
523
|
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
|
|
541
524
|
elif bp.find('a:spAutoFit', NS) is not None:
|
|
542
525
|
b['autofit'] = 'shape'
|
|
543
526
|
if b:
|