@lark-apaas/coding-steering 0.1.32-dev.5abff3b → 0.1.32-dev.7b2c2ad
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 +13 -6
- package/steering/design-html/skills/pptx-style-extract/scripts/census.py +8 -2
- package/steering/design-html/skills/pptx-style-extract/scripts/draft.py +121 -30
- package/steering/design-html/skills/pptx-style-extract/scripts/extract.py +229 -10
- package/steering/design-html/skills/pptx-style-extract/scripts/parts.py +19 -3
- package/steering/design-html/skills/pptx-style-extract/scripts/render_pages.py +4 -2
- package/steering/design-html/skills/pptx-style-extract/scripts/test_asset_judgment_package.py +113 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_background_composite.py +308 -1
- package/steering/design-html/skills/pptx-style-extract/scripts/test_design_consumer_contract.py +13 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_layout_css.py +301 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_logo_scope.py +600 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/verify_layout_assets.py +421 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/verify_logo_scope.py +12 -0
|
@@ -181,6 +181,8 @@ 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')
|
|
184
186
|
if 0 < (info.get('max_w_pct') or 0) < SMALL_IMG_W_PCT and not info['fullscreen']:
|
|
185
187
|
# 小图(页内图标、角标)。不导出的话 L 层只能看着装饰容器里的空洞
|
|
186
188
|
# 自己编图形,编出来的与模板无关。
|
|
@@ -261,6 +263,30 @@ def _is_full_canvas_picture(shape):
|
|
|
261
263
|
and (shape.get('h_pct') or 0) >= 95)
|
|
262
264
|
|
|
263
265
|
|
|
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
|
+
|
|
264
290
|
def _picture_has_appearance(shape):
|
|
265
291
|
return bool(shape.get('flipH') or shape.get('flipV') or shape.get('rot')
|
|
266
292
|
or shape.get('crop')
|
|
@@ -333,6 +359,147 @@ def _draw_picture(canvas, shape, pkg, image_cache=None):
|
|
|
333
359
|
return True
|
|
334
360
|
|
|
335
361
|
|
|
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
|
+
|
|
336
503
|
def _draw_background(canvas, background, pkg, image_cache=None):
|
|
337
504
|
"""Draw the effective p:bg underneath picture layers."""
|
|
338
505
|
from PIL import Image
|
|
@@ -353,6 +520,48 @@ def _draw_background(canvas, background, pkg, image_cache=None):
|
|
|
353
520
|
}, pkg, image_cache)
|
|
354
521
|
|
|
355
522
|
|
|
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
|
+
|
|
356
565
|
def _save_background_webp(canvas, path):
|
|
357
566
|
"""Prefer lossless output; fall back to a quality ladder when oversized."""
|
|
358
567
|
canvas.convert('RGB').save(path, 'WEBP', lossless=True, method=0)
|
|
@@ -398,15 +607,12 @@ def compose_backgrounds(pkg, graph, shapes, bg_by_part, units, outdir, pillow_ok
|
|
|
398
607
|
image_cache = {}
|
|
399
608
|
for target, chain in chains.items():
|
|
400
609
|
layers = [shape for part in chain for shape in by_part.get(part, [])
|
|
401
|
-
if _is_full_canvas_picture(shape)]
|
|
402
|
-
|
|
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):
|
|
403
614
|
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]
|
|
615
|
+
source_layers = [_background_layer_source(shape) for shape in layers]
|
|
410
616
|
spec = json.dumps({
|
|
411
617
|
'background': _effective_background(chain, bg_by_part),
|
|
412
618
|
'layers': [{k: v for k, v in layer.items() if k != 'part'}
|
|
@@ -417,8 +623,12 @@ def compose_backgrounds(pkg, graph, shapes, bg_by_part, units, outdir, pillow_ok
|
|
|
417
623
|
canvas = Image.new('RGBA', (units.w, units.h))
|
|
418
624
|
_draw_background(
|
|
419
625
|
canvas, _effective_background(chain, bg_by_part), pkg, image_cache)
|
|
420
|
-
drawn = [
|
|
421
|
-
|
|
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
|
+
]
|
|
422
632
|
if len(drawn) != len(layers):
|
|
423
633
|
continue
|
|
424
634
|
digest = hashlib.sha256(canvas.tobytes()).hexdigest()[:16]
|
|
@@ -826,6 +1036,9 @@ def extract(pptx, outdir, export_all=False):
|
|
|
826
1036
|
'box_emu': {'x': 0, 'y': 0, 'cx': cx, 'cy': cy},
|
|
827
1037
|
'crop': bg.get('crop'), 'placement': 'inside',
|
|
828
1038
|
'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)
|
|
829
1042
|
images, variant_groups = image_census(shapes, bg_images, units) # S5
|
|
830
1043
|
t = mark('S5_image_census', t)
|
|
831
1044
|
|
|
@@ -845,6 +1058,10 @@ def extract(pptx, outdir, export_all=False):
|
|
|
845
1058
|
t = mark('derived_censuses', t)
|
|
846
1059
|
|
|
847
1060
|
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
|
|
848
1065
|
background_composites, composite_media, composite_images = compose_backgrounds(
|
|
849
1066
|
pkg, graph, shapes, bg_by_part, units, outdir, pillow_ok)
|
|
850
1067
|
media_rows += composite_media
|
|
@@ -949,6 +1166,8 @@ def extract(pptx, outdir, export_all=False):
|
|
|
949
1166
|
'content_clusters_multi_media': sum(1 for c in clusters if c['member_n'] > 1),
|
|
950
1167
|
'media_exported': sum(1 for m in media_rows
|
|
951
1168
|
if m.get('exported') and not m.get('generated')),
|
|
1169
|
+
'media_instances': len(instance_media),
|
|
1170
|
+
'media_instance_blocked': len(instance_blocked),
|
|
952
1171
|
'media_transcoded': sum(1 for m in media_rows
|
|
953
1172
|
if m.get('transcoded') and not m.get('generated')),
|
|
954
1173
|
'media_transcode_blocked': sum(1 for m in media_rows
|
|
@@ -299,6 +299,22 @@ NV_TAGS = {'sp': 'p:nvSpPr', 'pic': 'p:nvPicPr', 'grpSp': 'p:nvGrpSpPr',
|
|
|
299
299
|
'cxnSp': 'p:nvCxnSpPr', 'graphicFrame': 'p:nvGraphicFramePr'}
|
|
300
300
|
|
|
301
301
|
|
|
302
|
+
def blip_opacity(blip):
|
|
303
|
+
"""Combine the alpha modulation transforms attached to one local picture."""
|
|
304
|
+
opacity = 1.0
|
|
305
|
+
found = False
|
|
306
|
+
for effect in blip:
|
|
307
|
+
if local(effect.tag) not in ('alphaMod', 'alphaModFix'):
|
|
308
|
+
continue
|
|
309
|
+
try:
|
|
310
|
+
amount = int(effect.get('amt'))
|
|
311
|
+
except (TypeError, ValueError):
|
|
312
|
+
continue
|
|
313
|
+
opacity *= max(0.0, min(amount / 100000.0, 1.0))
|
|
314
|
+
found = True
|
|
315
|
+
return round(opacity, 6) if found else None
|
|
316
|
+
|
|
317
|
+
|
|
302
318
|
def walk_tree(el, ctx, out, path=(), xf=(1.0, 1.0, 0.0, 0.0), depth=0):
|
|
303
319
|
sx, sy, dx, dy = xf
|
|
304
320
|
U, W, H = ctx.units, ctx.units.w, ctx.units.h
|
|
@@ -393,9 +409,9 @@ def walk_tree(el, ctx, out, path=(), xf=(1.0, 1.0, 0.0, 0.0), depth=0):
|
|
|
393
409
|
blip = sp.find('p:blipFill/a:blip', NS)
|
|
394
410
|
if blip is not None:
|
|
395
411
|
rec['media'] = ctx.media_of(blip.get(R_EMBED)) or ctx.media_of(blip.get(R_LINK))
|
|
396
|
-
|
|
397
|
-
if
|
|
398
|
-
rec['opacity'] =
|
|
412
|
+
opacity = blip_opacity(blip)
|
|
413
|
+
if opacity is not None:
|
|
414
|
+
rec['opacity'] = opacity
|
|
399
415
|
svg = blip.find('a:extLst//asvg:svgBlip', NS)
|
|
400
416
|
if svg is not None:
|
|
401
417
|
rec['media_svg'] = ctx.media_of(svg.get(R_EMBED))
|
|
@@ -88,13 +88,15 @@ def css_gradient(grad):
|
|
|
88
88
|
def background_css(bg):
|
|
89
89
|
"""页底 <p:bg> 描述符 → CSS background 值。解析不出返回 None,交上层回退到下一层底色。
|
|
90
90
|
|
|
91
|
-
bgPr solid / bgRef 都带 `color
|
|
92
|
-
|
|
91
|
+
bgPr solid / bgRef 都带 `color`;线性 gradient 与形状渐变共用 css_gradient;
|
|
92
|
+
path 渐变和背景图不重建,按无底色处理,不能伪装成线性渐变。
|
|
93
93
|
"""
|
|
94
94
|
if not bg:
|
|
95
95
|
return None
|
|
96
96
|
kind = bg.get('type')
|
|
97
97
|
if kind == 'gradient':
|
|
98
|
+
if bg.get('path'):
|
|
99
|
+
return None
|
|
98
100
|
return css_gradient(bg)
|
|
99
101
|
if kind in ('image', 'none', 'pattern'):
|
|
100
102
|
return None
|
package/steering/design-html/skills/pptx-style-extract/scripts/test_asset_judgment_package.py
CHANGED
|
@@ -438,6 +438,119 @@ class AssetJudgmentPackageTest(unittest.TestCase):
|
|
|
438
438
|
if result.fails
|
|
439
439
|
])
|
|
440
440
|
|
|
441
|
+
def test_package_places_a_rendered_picture_instance_as_a_logo_asset(self):
|
|
442
|
+
try:
|
|
443
|
+
from PIL import Image
|
|
444
|
+
except ImportError:
|
|
445
|
+
self.skipTest('Pillow unavailable')
|
|
446
|
+
|
|
447
|
+
with tempfile.TemporaryDirectory() as root:
|
|
448
|
+
stage1 = os.path.join(root, 'stage1')
|
|
449
|
+
lout = os.path.join(stage1, 'l-out')
|
|
450
|
+
pack = os.path.join(root, 'pack')
|
|
451
|
+
media = os.path.join(stage1, 'media-out')
|
|
452
|
+
ref = os.path.join(stage1, 'ref')
|
|
453
|
+
os.makedirs(lout)
|
|
454
|
+
os.makedirs(media)
|
|
455
|
+
os.makedirs(ref)
|
|
456
|
+
|
|
457
|
+
source = 'generated/instance/picture-directory-logo.webp'
|
|
458
|
+
output = 'picture-directory-logo.webp'
|
|
459
|
+
box = [1813, 18, 59, 228]
|
|
460
|
+
Image.new('RGBA', (59, 228), (20, 60, 100, 255)).save(
|
|
461
|
+
os.path.join(media, output), 'WEBP')
|
|
462
|
+
extract = {
|
|
463
|
+
'source': {'filename': 'fixture.pptx'},
|
|
464
|
+
'canvas': {
|
|
465
|
+
'px': [1920, 1080],
|
|
466
|
+
'source': {'cx': 12192000, 'cy': 6858000, 'unit': 'EMU'},
|
|
467
|
+
},
|
|
468
|
+
'media': [{
|
|
469
|
+
'media': source,
|
|
470
|
+
'out': 'media-out/' + output,
|
|
471
|
+
'generated': True,
|
|
472
|
+
'rendered_from': 'ppt/media/original-logo.png',
|
|
473
|
+
}],
|
|
474
|
+
'images': [{
|
|
475
|
+
'media': source,
|
|
476
|
+
'boxes': [{
|
|
477
|
+
'count': 1,
|
|
478
|
+
'box': dict(zip(('x', 'y', 'w', 'h'), box)),
|
|
479
|
+
'parts': ['ppt/slides/slide2.xml'],
|
|
480
|
+
}],
|
|
481
|
+
}],
|
|
482
|
+
'color_freq': [],
|
|
483
|
+
'text_scale': [],
|
|
484
|
+
}
|
|
485
|
+
with open(os.path.join(stage1, 'extract.json'), 'w', encoding='utf-8') as stream:
|
|
486
|
+
json.dump(extract, stream)
|
|
487
|
+
with open(os.path.join(ref, 'shapes.json'), 'w', encoding='utf-8') as stream:
|
|
488
|
+
json.dump({'shapes': [{
|
|
489
|
+
'part': 'ppt/slides/slide2.xml',
|
|
490
|
+
'kind': 'pic',
|
|
491
|
+
'box': dict(zip(('x', 'y', 'w', 'h'), box)),
|
|
492
|
+
}]}, stream)
|
|
493
|
+
with open(os.path.join(lout, 'asset-vision-groups.json'),
|
|
494
|
+
'w', encoding='utf-8') as stream:
|
|
495
|
+
json.dump({
|
|
496
|
+
'version': 2,
|
|
497
|
+
'selected': [{
|
|
498
|
+
'candidates': [{
|
|
499
|
+
'id': 'asset-directory-logo',
|
|
500
|
+
'source_media': output,
|
|
501
|
+
'placements': [{
|
|
502
|
+
'slide': 2,
|
|
503
|
+
'archetype': 'content',
|
|
504
|
+
'box': box,
|
|
505
|
+
}],
|
|
506
|
+
}],
|
|
507
|
+
}],
|
|
508
|
+
'omitted': [],
|
|
509
|
+
}, stream)
|
|
510
|
+
with open(os.path.join(lout, 'manifest.yaml'), 'w', encoding='utf-8') as stream:
|
|
511
|
+
stream.write(
|
|
512
|
+
'version: alpha\n'
|
|
513
|
+
'name: rendered-instance-fixture\n'
|
|
514
|
+
'description: Rendered local PPT image instances remain reusable assets.\n'
|
|
515
|
+
'asset_vision_groups:\n'
|
|
516
|
+
' - id: asset-directory-logo\n'
|
|
517
|
+
' source_media: picture-directory-logo.webp\n'
|
|
518
|
+
' box: [1813, 18, 59, 228]\n'
|
|
519
|
+
' visual_kind: logo\n'
|
|
520
|
+
)
|
|
521
|
+
for name, content in (
|
|
522
|
+
('frontmatter.yaml', ''),
|
|
523
|
+
('body.md', 'Read `layouts.md` before composing a slide.\n\n{{ASSET_TABLE}}\n'),
|
|
524
|
+
('layouts.yaml',
|
|
525
|
+
'layouts:\n'
|
|
526
|
+
' content:\n'
|
|
527
|
+
' name: "目录"\n'
|
|
528
|
+
' role: content\n'
|
|
529
|
+
' slots:\n'
|
|
530
|
+
' - {role: asset-candidate, box: [1813, 18, 59, 228], type: pic, '
|
|
531
|
+
'source_media: picture-directory-logo.webp}\n'
|
|
532
|
+
' confidence: high\n'),
|
|
533
|
+
):
|
|
534
|
+
with open(os.path.join(lout, name), 'w', encoding='utf-8') as stream:
|
|
535
|
+
stream.write(content)
|
|
536
|
+
check_v1 = os.path.join(root, 'check_v1.py')
|
|
537
|
+
with open(check_v1, 'w', encoding='utf-8') as stream:
|
|
538
|
+
stream.write('raise SystemExit(0)\n')
|
|
539
|
+
|
|
540
|
+
self.assertEqual(package_main([stage1, lout, pack, '--check-v1', check_v1]), 0)
|
|
541
|
+
|
|
542
|
+
with open(os.path.join(pack, 'design.md'), encoding='utf-8') as stream:
|
|
543
|
+
design = stream.read()
|
|
544
|
+
with open(os.path.join(pack, 'layouts.md'), encoding='utf-8') as stream:
|
|
545
|
+
layouts = stream.read()
|
|
546
|
+
self.assertTrue(os.path.isfile(os.path.join(pack, 'assets', 'logos', '1.webp')))
|
|
547
|
+
self.assertIn('logo-1:', design)
|
|
548
|
+
self.assertIn('assets/logos/1.webp', design)
|
|
549
|
+
self.assertIn(
|
|
550
|
+
'- {role: logo, box: [1813, 18, 59, 228], type: pic, asset: logo-1}',
|
|
551
|
+
layouts,
|
|
552
|
+
)
|
|
553
|
+
|
|
441
554
|
|
|
442
555
|
if __name__ == '__main__':
|
|
443
556
|
unittest.main()
|