@lark-apaas/coding-steering 0.1.32-dev.4f80f68 → 0.1.32-dev.942e73f
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 +27 -18
- package/steering/design-html/skills/pptx-style-extract/scripts/census.py +8 -2
- package/steering/design-html/skills/pptx-style-extract/scripts/draft.py +1152 -178
- package/steering/design-html/skills/pptx-style-extract/scripts/extract.py +229 -10
- package/steering/design-html/skills/pptx-style-extract/scripts/ooxml.py +18 -1
- package/steering/design-html/skills/pptx-style-extract/scripts/package.py +643 -40
- 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 +556 -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 +6 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_flow_layout_contract.py +157 -7
- package/steering/design-html/skills/pptx-style-extract/scripts/test_layout_css.py +1519 -2
- package/steering/design-html/skills/pptx-style-extract/scripts/test_logo_scope.py +148 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_text_role_contract.py +151 -4
- package/steering/design-html/skills/pptx-style-extract/scripts/verify_logo_scope.py +188 -0
- package/steering/design-html/skills/pptx-style-extract/v2-format-spec.md +1 -1
|
@@ -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
|
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Integration regression tests for model-decided image roles."""
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import tempfile
|
|
7
|
+
import unittest
|
|
8
|
+
|
|
9
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
10
|
+
|
|
11
|
+
from check_v2 import Pack, RULES # noqa: E402
|
|
12
|
+
from package import (Fail, apply_asset_decisions, bind_asset_vision_group_scopes,
|
|
13
|
+
load_layout_blocks, main as package_main, # noqa: E402
|
|
14
|
+
validate_asset_vision_groups)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AssetJudgmentPackageTest(unittest.TestCase):
|
|
18
|
+
def test_asset_vision_index_requires_selected_candidates(self):
|
|
19
|
+
with tempfile.TemporaryDirectory() as lout:
|
|
20
|
+
with open(os.path.join(lout, 'asset-vision-groups.json'),
|
|
21
|
+
'w', encoding='utf-8') as stream:
|
|
22
|
+
json.dump({'version': 2}, stream)
|
|
23
|
+
|
|
24
|
+
with self.assertRaisesRegex(Fail, '缺少 selected'):
|
|
25
|
+
validate_asset_vision_groups(lout, {'asset_vision_groups': []})
|
|
26
|
+
|
|
27
|
+
def test_asset_vision_groups_must_keep_every_extracted_candidate(self):
|
|
28
|
+
with tempfile.TemporaryDirectory() as lout:
|
|
29
|
+
with open(os.path.join(lout, 'asset-vision-groups.json'),
|
|
30
|
+
'w', encoding='utf-8') as stream:
|
|
31
|
+
json.dump({
|
|
32
|
+
'version': 2,
|
|
33
|
+
'selected': [{
|
|
34
|
+
'candidates': [{
|
|
35
|
+
'id': 'asset-cover',
|
|
36
|
+
'source_media': 'cover.png',
|
|
37
|
+
'placements': [{
|
|
38
|
+
'box': [100, 200, 300, 400],
|
|
39
|
+
'archetype': 'cover',
|
|
40
|
+
}],
|
|
41
|
+
}, {
|
|
42
|
+
'id': 'asset-ornament',
|
|
43
|
+
'source_media': 'ornament.png',
|
|
44
|
+
'placements': [{
|
|
45
|
+
'box': [10, 20, 30, 40],
|
|
46
|
+
'archetype': 'content',
|
|
47
|
+
}],
|
|
48
|
+
}],
|
|
49
|
+
}],
|
|
50
|
+
}, stream)
|
|
51
|
+
manifest = {
|
|
52
|
+
'asset_vision_groups': [{
|
|
53
|
+
'id': 'asset-cover',
|
|
54
|
+
'source_media': 'cover.png',
|
|
55
|
+
'layout': 'cover',
|
|
56
|
+
'box': '[100, 200, 300, 400]',
|
|
57
|
+
'visual_kind': 'illustration',
|
|
58
|
+
}, {
|
|
59
|
+
'id': 'asset-ornament',
|
|
60
|
+
'source_media': 'ornament.png',
|
|
61
|
+
'layout': 'content',
|
|
62
|
+
'box': '[10, 20, 30, 40]',
|
|
63
|
+
'visual_kind': 'decorative',
|
|
64
|
+
}],
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
validate_asset_vision_groups(lout, manifest)
|
|
68
|
+
manifest['asset_vision_groups'].pop()
|
|
69
|
+
|
|
70
|
+
with self.assertRaisesRegex(Fail, '逐项保留抽取候选.*asset-ornament'):
|
|
71
|
+
validate_asset_vision_groups(lout, manifest)
|
|
72
|
+
|
|
73
|
+
def test_asset_vision_groups_reject_rewritten_extracted_candidate_scope(self):
|
|
74
|
+
with tempfile.TemporaryDirectory() as lout:
|
|
75
|
+
with open(os.path.join(lout, 'asset-vision-groups.json'),
|
|
76
|
+
'w', encoding='utf-8') as stream:
|
|
77
|
+
json.dump({
|
|
78
|
+
'version': 2,
|
|
79
|
+
'selected': [{
|
|
80
|
+
'candidates': [{
|
|
81
|
+
'id': 'asset-ornament',
|
|
82
|
+
'source_media': 'ornament.png',
|
|
83
|
+
'placements': [{
|
|
84
|
+
'box': [10, 20, 30, 40],
|
|
85
|
+
'archetype': None,
|
|
86
|
+
}],
|
|
87
|
+
}],
|
|
88
|
+
}],
|
|
89
|
+
}, stream)
|
|
90
|
+
manifest = {
|
|
91
|
+
'asset_vision_groups': [{
|
|
92
|
+
'id': 'asset-ornament',
|
|
93
|
+
'source_media': 'other.png',
|
|
94
|
+
'box': '[10, 20, 30, 40]',
|
|
95
|
+
'visual_kind': 'decorative',
|
|
96
|
+
}],
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
with self.assertRaisesRegex(Fail, '改写了抽取候选的图片或位置'):
|
|
100
|
+
validate_asset_vision_groups(lout, manifest)
|
|
101
|
+
|
|
102
|
+
def test_v2_candidate_ids_resolve_same_box_on_different_layouts(self):
|
|
103
|
+
with tempfile.TemporaryDirectory() as lout:
|
|
104
|
+
with open(os.path.join(lout, 'asset-vision-groups.json'),
|
|
105
|
+
'w', encoding='utf-8') as stream:
|
|
106
|
+
json.dump({
|
|
107
|
+
'version': 2,
|
|
108
|
+
'selected': [{
|
|
109
|
+
'candidates': [{
|
|
110
|
+
'id': 'asset-cover-logo',
|
|
111
|
+
'source_media': 'shared.png',
|
|
112
|
+
'placements': [{
|
|
113
|
+
'box': [100, 200, 80, 80],
|
|
114
|
+
'archetype': 'cover',
|
|
115
|
+
}],
|
|
116
|
+
}, {
|
|
117
|
+
'id': 'asset-content-ornament',
|
|
118
|
+
'source_media': 'shared.png',
|
|
119
|
+
'placements': [{
|
|
120
|
+
'box': [100, 200, 80, 80],
|
|
121
|
+
'archetype': 'content',
|
|
122
|
+
}],
|
|
123
|
+
}],
|
|
124
|
+
}],
|
|
125
|
+
}, stream)
|
|
126
|
+
manifest = {
|
|
127
|
+
'assets': [],
|
|
128
|
+
'asset_vision_groups': [{
|
|
129
|
+
'id': 'asset-cover-logo',
|
|
130
|
+
'source_media': 'shared.png',
|
|
131
|
+
'box': '[100, 200, 80, 80]',
|
|
132
|
+
'visual_kind': 'logo',
|
|
133
|
+
}, {
|
|
134
|
+
'id': 'asset-content-ornament',
|
|
135
|
+
'source_media': 'shared.png',
|
|
136
|
+
'box': '[100, 200, 80, 80]',
|
|
137
|
+
'visual_kind': 'decorative',
|
|
138
|
+
}],
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
bind_asset_vision_group_scopes(lout, manifest)
|
|
142
|
+
decisions, _ = apply_asset_decisions(
|
|
143
|
+
manifest,
|
|
144
|
+
bound_sources={'shared.png'},
|
|
145
|
+
bound_slots={
|
|
146
|
+
'shared.png': {
|
|
147
|
+
('cover', (100, 200, 80, 80)),
|
|
148
|
+
('content', (100, 200, 80, 80)),
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
self.assertEqual(
|
|
154
|
+
decisions.for_scope('shared.png', 'cover', (100, 200, 80, 80)),
|
|
155
|
+
'logo',
|
|
156
|
+
)
|
|
157
|
+
self.assertEqual(
|
|
158
|
+
decisions.for_scope('shared.png', 'content', (100, 200, 80, 80)),
|
|
159
|
+
'texture',
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
def test_legacy_asset_vision_index_accepts_a_group_default(self):
|
|
163
|
+
with tempfile.TemporaryDirectory() as lout:
|
|
164
|
+
with open(os.path.join(lout, 'asset-vision-groups.json'),
|
|
165
|
+
'w', encoding='utf-8') as stream:
|
|
166
|
+
json.dump({
|
|
167
|
+
'version': 1,
|
|
168
|
+
'selected': [{
|
|
169
|
+
'candidates': [{
|
|
170
|
+
'id': 'asset-cover',
|
|
171
|
+
'source_media': 'cover.png',
|
|
172
|
+
'placements': [{'box': [0, 0, 100, 100]}],
|
|
173
|
+
}, {
|
|
174
|
+
'id': 'asset-ornament',
|
|
175
|
+
'source_media': 'ornament.png',
|
|
176
|
+
'placements': [{'box': [10, 20, 30, 40]}],
|
|
177
|
+
}],
|
|
178
|
+
}],
|
|
179
|
+
}, stream)
|
|
180
|
+
manifest = {
|
|
181
|
+
'asset_vision_groups': [{
|
|
182
|
+
'id': 'vision-1',
|
|
183
|
+
'source_media': '[cover.png, ornament.png]',
|
|
184
|
+
'visual_kind': 'decorative',
|
|
185
|
+
}],
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
validate_asset_vision_groups(lout, manifest)
|
|
189
|
+
|
|
190
|
+
def test_layout_controls_override_legacy_judgment_without_reading_layout_todos(self):
|
|
191
|
+
with tempfile.TemporaryDirectory() as root:
|
|
192
|
+
lout = os.path.join(root, 'l-out')
|
|
193
|
+
os.makedirs(lout)
|
|
194
|
+
with open(os.path.join(lout, 'layouts.yaml'), 'w', encoding='utf-8') as stream:
|
|
195
|
+
stream.write(
|
|
196
|
+
'names:\n'
|
|
197
|
+
' content: TODO中文名\n'
|
|
198
|
+
'roles:\n'
|
|
199
|
+
' content: TODO角色\n'
|
|
200
|
+
'layouts:\n'
|
|
201
|
+
' content:\n'
|
|
202
|
+
' slots:\n'
|
|
203
|
+
' - {role: body, box: [10, 20, 30, 40], type: body}\n'
|
|
204
|
+
' confidence: high\n'
|
|
205
|
+
)
|
|
206
|
+
with open(os.path.join(lout, 'layout-controls.yaml'), 'w', encoding='utf-8') as stream:
|
|
207
|
+
stream.write(
|
|
208
|
+
'names:\n'
|
|
209
|
+
' content: "内容页"\n'
|
|
210
|
+
'roles:\n'
|
|
211
|
+
' content: content\n'
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
blocks = load_layout_blocks(lout)
|
|
215
|
+
|
|
216
|
+
self.assertEqual([key for key, _, _ in blocks],
|
|
217
|
+
['names', 'roles', 'layouts'])
|
|
218
|
+
self.assertIn('内容页', blocks[0][2][0])
|
|
219
|
+
self.assertIn('content', blocks[1][2][0])
|
|
220
|
+
self.assertNotIn('TODO', '\n'.join(
|
|
221
|
+
line for _, _, lines in blocks for line in lines
|
|
222
|
+
))
|
|
223
|
+
|
|
224
|
+
def test_layout_controls_reject_non_judgment_block(self):
|
|
225
|
+
with tempfile.TemporaryDirectory() as root:
|
|
226
|
+
lout = os.path.join(root, 'l-out')
|
|
227
|
+
os.makedirs(lout)
|
|
228
|
+
with open(os.path.join(lout, 'layouts.yaml'), 'w', encoding='utf-8') as stream:
|
|
229
|
+
stream.write('layouts:\n content:\n role: content\n')
|
|
230
|
+
with open(os.path.join(lout, 'layout-controls.yaml'), 'w', encoding='utf-8') as stream:
|
|
231
|
+
stream.write('layouts:\n cover:\n role: cover\n')
|
|
232
|
+
|
|
233
|
+
with self.assertRaisesRegex(Fail, '只能包含'):
|
|
234
|
+
load_layout_blocks(lout)
|
|
235
|
+
|
|
236
|
+
def test_layout_controls_must_cover_all_legacy_judgment_blocks(self):
|
|
237
|
+
with tempfile.TemporaryDirectory() as root:
|
|
238
|
+
lout = os.path.join(root, 'l-out')
|
|
239
|
+
os.makedirs(lout)
|
|
240
|
+
with open(os.path.join(lout, 'layouts.yaml'), 'w', encoding='utf-8') as stream:
|
|
241
|
+
stream.write(
|
|
242
|
+
'names:\n'
|
|
243
|
+
' content: "内容页"\n'
|
|
244
|
+
'roles:\n'
|
|
245
|
+
' content: content\n'
|
|
246
|
+
'layouts:\n'
|
|
247
|
+
' content:\n'
|
|
248
|
+
' role: content\n'
|
|
249
|
+
)
|
|
250
|
+
with open(os.path.join(lout, 'layout-controls.yaml'), 'w',
|
|
251
|
+
encoding='utf-8') as stream:
|
|
252
|
+
stream.write('roles:\n content: content\n')
|
|
253
|
+
|
|
254
|
+
with self.assertRaisesRegex(Fail, '缺:names'):
|
|
255
|
+
load_layout_blocks(lout)
|
|
256
|
+
|
|
257
|
+
def test_layout_controls_must_keep_each_legacy_judgment_entry(self):
|
|
258
|
+
with tempfile.TemporaryDirectory() as root:
|
|
259
|
+
lout = os.path.join(root, 'l-out')
|
|
260
|
+
os.makedirs(lout)
|
|
261
|
+
with open(os.path.join(lout, 'layouts.yaml'), 'w', encoding='utf-8') as stream:
|
|
262
|
+
stream.write(
|
|
263
|
+
'names:\n'
|
|
264
|
+
' cover: "封面"\n'
|
|
265
|
+
' content: "内容页"\n'
|
|
266
|
+
'roles:\n'
|
|
267
|
+
' cover: cover\n'
|
|
268
|
+
' content: content\n'
|
|
269
|
+
'layouts:\n'
|
|
270
|
+
' cover:\n'
|
|
271
|
+
' role: cover\n'
|
|
272
|
+
' content:\n'
|
|
273
|
+
' role: content\n'
|
|
274
|
+
)
|
|
275
|
+
with open(os.path.join(lout, 'layout-controls.yaml'), 'w',
|
|
276
|
+
encoding='utf-8') as stream:
|
|
277
|
+
stream.write(
|
|
278
|
+
'names:\n'
|
|
279
|
+
' cover: "封面"\n'
|
|
280
|
+
'roles:\n'
|
|
281
|
+
' cover: cover\n'
|
|
282
|
+
' content: content\n'
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
with self.assertRaisesRegex(Fail, 'names 段缺少旧判断条目:content'):
|
|
286
|
+
load_layout_blocks(lout)
|
|
287
|
+
|
|
288
|
+
def test_package_strips_judgment_fields_and_preserves_content_slot(self):
|
|
289
|
+
try:
|
|
290
|
+
from PIL import Image
|
|
291
|
+
except ImportError:
|
|
292
|
+
self.skipTest('Pillow unavailable')
|
|
293
|
+
|
|
294
|
+
with tempfile.TemporaryDirectory() as root:
|
|
295
|
+
stage1 = os.path.join(root, 'stage1')
|
|
296
|
+
lout = os.path.join(stage1, 'l-out')
|
|
297
|
+
pack = os.path.join(root, 'pack')
|
|
298
|
+
media = os.path.join(stage1, 'media-out')
|
|
299
|
+
ref = os.path.join(stage1, 'ref')
|
|
300
|
+
os.makedirs(lout)
|
|
301
|
+
os.makedirs(media)
|
|
302
|
+
os.makedirs(ref)
|
|
303
|
+
|
|
304
|
+
Image.new('RGBA', (40, 20), (20, 40, 60, 255)).save(
|
|
305
|
+
os.path.join(media, 'partner-logo.png'))
|
|
306
|
+
Image.new('RGBA', (24, 24), (60, 40, 20, 255)).save(
|
|
307
|
+
os.path.join(media, 'ornament.png'))
|
|
308
|
+
|
|
309
|
+
box_rows = [
|
|
310
|
+
('ppt/media/image1.png', 'partner-logo.png', [100, 200, 400, 200]),
|
|
311
|
+
('ppt/media/image2.png', 'ornament.png', [40, 40, 120, 120]),
|
|
312
|
+
]
|
|
313
|
+
extract = {
|
|
314
|
+
'source': {'filename': 'fixture.pptx'},
|
|
315
|
+
'canvas': {
|
|
316
|
+
'px': [1920, 1080],
|
|
317
|
+
'source': {'cx': 12192000, 'cy': 6858000, 'unit': 'EMU'},
|
|
318
|
+
},
|
|
319
|
+
'media': [
|
|
320
|
+
{
|
|
321
|
+
'media': source,
|
|
322
|
+
'out': 'media-out/' + output,
|
|
323
|
+
'source_px': [40, 20] if output == 'partner-logo.png' else [24, 24],
|
|
324
|
+
}
|
|
325
|
+
for source, output, _ in box_rows
|
|
326
|
+
],
|
|
327
|
+
'images': [
|
|
328
|
+
{
|
|
329
|
+
'media': source,
|
|
330
|
+
'boxes': [{
|
|
331
|
+
'count': 1,
|
|
332
|
+
'box': {'x': box[0], 'y': box[1], 'w': box[2], 'h': box[3]},
|
|
333
|
+
'parts': ['ppt/slides/slide1.xml'],
|
|
334
|
+
}],
|
|
335
|
+
}
|
|
336
|
+
for source, _, box in box_rows
|
|
337
|
+
],
|
|
338
|
+
'color_freq': [],
|
|
339
|
+
'text_scale': [],
|
|
340
|
+
}
|
|
341
|
+
with open(os.path.join(stage1, 'extract.json'), 'w', encoding='utf-8') as stream:
|
|
342
|
+
json.dump(extract, stream)
|
|
343
|
+
with open(os.path.join(ref, 'shapes.json'), 'w', encoding='utf-8') as stream:
|
|
344
|
+
json.dump({
|
|
345
|
+
'shapes': [
|
|
346
|
+
{
|
|
347
|
+
'part': 'ppt/slides/slide1.xml',
|
|
348
|
+
'kind': 'pic',
|
|
349
|
+
'name': output,
|
|
350
|
+
'box': {'x': box[0], 'y': box[1], 'w': box[2], 'h': box[3]},
|
|
351
|
+
}
|
|
352
|
+
for _, output, box in box_rows
|
|
353
|
+
],
|
|
354
|
+
}, stream)
|
|
355
|
+
|
|
356
|
+
with open(os.path.join(lout, 'manifest.yaml'), 'w', encoding='utf-8') as stream:
|
|
357
|
+
stream.write(
|
|
358
|
+
'version: alpha\n'
|
|
359
|
+
'name: asset-judgment-fixture\n'
|
|
360
|
+
'description: >\n'
|
|
361
|
+
' A restrained fixture with explicit image-role judgments and traced layout slots.\n'
|
|
362
|
+
'assets:\n'
|
|
363
|
+
' - id: logo-primary\n'
|
|
364
|
+
' source_media: partner-logo.png\n'
|
|
365
|
+
' kind: logo\n'
|
|
366
|
+
' on-bg: light\n'
|
|
367
|
+
'asset_vision_groups:\n'
|
|
368
|
+
' - id: vision-1\n'
|
|
369
|
+
' source_media: [partner-logo.png, ornament.png]\n'
|
|
370
|
+
' visual_kind: content-image\n'
|
|
371
|
+
'asset_decisions:\n'
|
|
372
|
+
' - source_media: ornament.png\n'
|
|
373
|
+
' visual_kind: decorative\n'
|
|
374
|
+
)
|
|
375
|
+
with open(os.path.join(lout, 'frontmatter.yaml'), 'w', encoding='utf-8') as stream:
|
|
376
|
+
stream.write('')
|
|
377
|
+
with open(os.path.join(lout, 'layouts.yaml'), 'w', encoding='utf-8') as stream:
|
|
378
|
+
stream.write(
|
|
379
|
+
'roles:\n'
|
|
380
|
+
' content: TODO角色\n'
|
|
381
|
+
'layouts:\n'
|
|
382
|
+
' content:\n'
|
|
383
|
+
' name: "内容页"\n'
|
|
384
|
+
' role: content\n'
|
|
385
|
+
' slots:\n'
|
|
386
|
+
' - {role: logo, box: [100, 200, 400, 200], type: pic, '
|
|
387
|
+
'asset: logo-primary, source_media: partner-logo.png}\n'
|
|
388
|
+
' - {role: asset-candidate, box: [40, 40, 120, 120], type: pic, '
|
|
389
|
+
'source_media: ornament.png}\n'
|
|
390
|
+
' confidence: high\n'
|
|
391
|
+
)
|
|
392
|
+
with open(os.path.join(lout, 'layout-controls.yaml'), 'w',
|
|
393
|
+
encoding='utf-8') as stream:
|
|
394
|
+
stream.write('roles:\n content: content\n')
|
|
395
|
+
with open(os.path.join(lout, 'body.md'), 'w', encoding='utf-8') as stream:
|
|
396
|
+
stream.write(
|
|
397
|
+
'## Usage\n\n'
|
|
398
|
+
'Read `layouts.md` before composing a slide.\n\n'
|
|
399
|
+
'## Assets\n\n'
|
|
400
|
+
'{{ASSET_TABLE}}\n\n'
|
|
401
|
+
'## Hard Rules\n\n'
|
|
402
|
+
'{{LOGO_RULES}}\n'
|
|
403
|
+
)
|
|
404
|
+
check_v1 = os.path.join(root, 'check_v1.py')
|
|
405
|
+
with open(check_v1, 'w', encoding='utf-8') as stream:
|
|
406
|
+
stream.write('raise SystemExit(0)\n')
|
|
407
|
+
|
|
408
|
+
rc = package_main([stage1, lout, pack, '--check-v1', check_v1])
|
|
409
|
+
|
|
410
|
+
self.assertEqual(rc, 0)
|
|
411
|
+
with open(os.path.join(pack, 'design.md'), encoding='utf-8') as stream:
|
|
412
|
+
design = stream.read()
|
|
413
|
+
with open(os.path.join(pack, 'layouts.md'), encoding='utf-8') as stream:
|
|
414
|
+
layouts = stream.read()
|
|
415
|
+
self.assertNotIn('asset_decisions', design)
|
|
416
|
+
self.assertNotIn('asset_vision_groups', design)
|
|
417
|
+
self.assertNotIn('visual_kind', design)
|
|
418
|
+
self.assertNotIn('source_media', design)
|
|
419
|
+
self.assertNotIn('source_media', layouts)
|
|
420
|
+
self.assertNotIn('TODO', layouts)
|
|
421
|
+
self.assertNotIn('{{LOGO_RULES}}', design)
|
|
422
|
+
self.assertNotIn('logo-primary', design)
|
|
423
|
+
self.assertNotIn('logo-primary', layouts)
|
|
424
|
+
self.assertIn(
|
|
425
|
+
'- {role: pic, box: [100, 200, 400, 200], type: pic}',
|
|
426
|
+
layouts,
|
|
427
|
+
)
|
|
428
|
+
self.assertIn('texture-1:', design)
|
|
429
|
+
self.assertIn('kind: texture', design)
|
|
430
|
+
self.assertIn(
|
|
431
|
+
'- {role: texture, box: [40, 40, 120, 120], type: pic, asset: texture-1}',
|
|
432
|
+
layouts,
|
|
433
|
+
)
|
|
434
|
+
gate = Pack(pack)
|
|
435
|
+
self.assertFalse([
|
|
436
|
+
result
|
|
437
|
+
for result in (rule(gate) for rule in RULES)
|
|
438
|
+
if result.fails
|
|
439
|
+
])
|
|
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
|
+
|
|
554
|
+
|
|
555
|
+
if __name__ == '__main__':
|
|
556
|
+
unittest.main()
|