@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.
Files changed (25) hide show
  1. package/package.json +6 -6
  2. package/steering/design-html/skills/pptx-style-extract/SKILL.md +19 -28
  3. package/steering/design-html/skills/pptx-style-extract/scripts/census.py +2 -8
  4. package/steering/design-html/skills/pptx-style-extract/scripts/draft.py +190 -1181
  5. package/steering/design-html/skills/pptx-style-extract/scripts/extract.py +10 -229
  6. package/steering/design-html/skills/pptx-style-extract/scripts/ooxml.py +1 -18
  7. package/steering/design-html/skills/pptx-style-extract/scripts/package.py +40 -643
  8. package/steering/design-html/skills/pptx-style-extract/scripts/parts.py +3 -19
  9. package/steering/design-html/skills/pptx-style-extract/scripts/render_pages.py +2 -4
  10. package/steering/design-html/skills/pptx-style-extract/scripts/test_background_composite.py +1 -308
  11. package/steering/design-html/skills/pptx-style-extract/scripts/test_design_consumer_contract.py +0 -14
  12. package/steering/design-html/skills/pptx-style-extract/scripts/test_flow_layout_contract.py +7 -157
  13. package/steering/design-html/skills/pptx-style-extract/scripts/test_layout_css.py +2 -1598
  14. package/steering/design-html/skills/pptx-style-extract/scripts/test_text_role_contract.py +4 -151
  15. package/steering/design-html/skills/pptx-style-extract/v2-format-spec.md +1 -1
  16. package/steering/design-html/skills/preflight/scripts/probe.sh +0 -0
  17. package/steering/nestjs-react-fullstack/skills/coding-guide/SKILL.md +0 -11
  18. package/steering/nestjs-react-fullstack/skills/contacts-service/SKILL.md +4 -67
  19. package/steering/nestjs-react-fullstack/skills/nestjs-cache/SKILL.md +1 -0
  20. package/steering/nestjs-react-fullstack/skills/openapi-guide/SKILL.md +0 -75
  21. package/steering/nestjs-react-fullstack/skills_local/coding-guide/SKILL.md +7 -5
  22. package/steering/design-html/skills/pptx-style-extract/scripts/test_asset_judgment_package.py +0 -556
  23. package/steering/design-html/skills/pptx-style-extract/scripts/test_logo_scope.py +0 -479
  24. package/steering/design-html/skills/pptx-style-extract/scripts/verify_layout_assets.py +0 -400
  25. 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) or _is_full_canvas_fill_overlay(shape)]
611
- pictures = [shape for shape in layers if shape.get('kind') == 'pic']
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 = [_background_layer_source(shape) for shape in 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
- shape for shape in layers
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
- na = bp.find('a:normAutofit', NS)
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: