@lark-apaas/coding-steering 0.1.32-dev.5abff3b → 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 +12 -5
- package/steering/design-html/skills/pptx-style-extract/scripts/census.py +8 -2
- package/steering/design-html/skills/pptx-style-extract/scripts/draft.py +67 -13
- 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 +5 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_layout_css.py +102 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_logo_scope.py +148 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/verify_logo_scope.py +188 -0
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()
|
|
@@ -3,18 +3,33 @@
|
|
|
3
3
|
import io
|
|
4
4
|
import os
|
|
5
5
|
import sys
|
|
6
|
+
import tempfile
|
|
6
7
|
import unittest
|
|
8
|
+
from unittest import mock
|
|
9
|
+
from xml.etree import ElementTree as ET
|
|
7
10
|
|
|
8
11
|
from PIL import Image
|
|
9
12
|
|
|
10
13
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
11
14
|
|
|
12
|
-
|
|
15
|
+
import extract # noqa: E402
|
|
16
|
+
from extract import ( # noqa: E402
|
|
17
|
+
_draw_picture,
|
|
18
|
+
_is_full_canvas_fill_overlay,
|
|
19
|
+
compose_backgrounds,
|
|
20
|
+
export_media,
|
|
21
|
+
export_visible_picture_instances,
|
|
22
|
+
)
|
|
23
|
+
from census import image_census # noqa: E402
|
|
24
|
+
from ooxml import Units # noqa: E402
|
|
25
|
+
from parts import walk_tree # noqa: E402
|
|
26
|
+
from render_pages import background_css # noqa: E402
|
|
13
27
|
|
|
14
28
|
|
|
15
29
|
class _Archive:
|
|
16
30
|
def __init__(self, media, raw):
|
|
17
31
|
self.names = {media}
|
|
32
|
+
self.media = [media]
|
|
18
33
|
self.zip = self
|
|
19
34
|
self.raw = raw
|
|
20
35
|
|
|
@@ -23,6 +38,35 @@ class _Archive:
|
|
|
23
38
|
raise KeyError(media)
|
|
24
39
|
return self.raw
|
|
25
40
|
|
|
41
|
+
def size_of(self, media):
|
|
42
|
+
if media not in self.names:
|
|
43
|
+
raise KeyError(media)
|
|
44
|
+
return len(self.raw)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class _CompositeArchive(_Archive):
|
|
48
|
+
def __init__(self, media, raw, slide):
|
|
49
|
+
super().__init__(media, raw)
|
|
50
|
+
self.layouts = []
|
|
51
|
+
self.slides = [slide]
|
|
52
|
+
|
|
53
|
+
@staticmethod
|
|
54
|
+
def xml(_part):
|
|
55
|
+
return {}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class _ShapeContext:
|
|
59
|
+
def __init__(self, media):
|
|
60
|
+
self.part = 'ppt/slides/slide1.xml'
|
|
61
|
+
self.layer = 'slide'
|
|
62
|
+
self.units = Units(1920, 1080)
|
|
63
|
+
self.clrmap = {}
|
|
64
|
+
self.clrscheme = {}
|
|
65
|
+
self.media = media
|
|
66
|
+
|
|
67
|
+
def media_of(self, _relationship_id):
|
|
68
|
+
return self.media
|
|
69
|
+
|
|
26
70
|
|
|
27
71
|
def _png(pixels):
|
|
28
72
|
image = Image.new('RGBA', (len(pixels), 1))
|
|
@@ -52,6 +96,269 @@ class BackgroundCompositeTest(unittest.TestCase):
|
|
|
52
96
|
self.assertEqual(left[3], 255)
|
|
53
97
|
self.assertEqual(right[3], 255)
|
|
54
98
|
|
|
99
|
+
def test_exports_a_cropped_rotated_visible_instance_from_a_near_blank_source(self):
|
|
100
|
+
media = 'ppt/media/source.png'
|
|
101
|
+
source = Image.new('RGBA', (1000, 1000))
|
|
102
|
+
for x in range(460, 540):
|
|
103
|
+
for y in range(460, 540):
|
|
104
|
+
source.putpixel((x, y), (15, 80, 160, 255))
|
|
105
|
+
raw = io.BytesIO()
|
|
106
|
+
source.save(raw, 'PNG')
|
|
107
|
+
package = _Archive(media, raw.getvalue())
|
|
108
|
+
shape = {
|
|
109
|
+
'kind': 'pic',
|
|
110
|
+
'media': media,
|
|
111
|
+
'part': 'ppt/slides/slide2.xml',
|
|
112
|
+
'box_unrotated': {'x': 100, 'y': 200, 'w': 20, 'h': 100},
|
|
113
|
+
'box': {'x': 50, 'y': 240, 'w': 120, 'h': 20},
|
|
114
|
+
'crop': {'l': 40, 't': 40, 'r': 40, 'b': 40},
|
|
115
|
+
'rot': 90,
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
with tempfile.TemporaryDirectory() as outdir:
|
|
119
|
+
rows = export_visible_picture_instances(package, [shape], outdir, True)
|
|
120
|
+
self.assertEqual(len(rows), 1)
|
|
121
|
+
self.assertTrue(rows[0]['generated'])
|
|
122
|
+
self.assertEqual(rows[0]['rendered_from'], media)
|
|
123
|
+
self.assertTrue(shape['media'].startswith('generated/instance/'))
|
|
124
|
+
|
|
125
|
+
rendered = Image.open(os.path.join(outdir, rows[0]['out'])).convert('RGBA')
|
|
126
|
+
self.assertGreater(rendered.width, rendered.height)
|
|
127
|
+
self.assertGreater(sum(pixel[3] for pixel in rendered.getdata()) / rendered.width
|
|
128
|
+
/ rendered.height, 13)
|
|
129
|
+
|
|
130
|
+
def test_instance_webp_writer_removes_partial_output_after_a_save_error(self):
|
|
131
|
+
canvas = Image.new('RGBA', (1, 1), (15, 80, 160, 255))
|
|
132
|
+
|
|
133
|
+
with tempfile.TemporaryDirectory() as outdir:
|
|
134
|
+
path = os.path.join(outdir, 'instance.webp')
|
|
135
|
+
with mock.patch.object(Image.Image, 'save', side_effect=OSError('webp unavailable')):
|
|
136
|
+
reason = extract._save_picture_instance_webp(canvas, path)
|
|
137
|
+
|
|
138
|
+
self.assertEqual('pillow-error: OSError: webp unavailable', reason)
|
|
139
|
+
self.assertFalse(os.path.exists(path))
|
|
140
|
+
self.assertFalse(os.path.exists(path + '.tmp'))
|
|
141
|
+
|
|
142
|
+
def test_instance_webp_failure_keeps_the_source_media_and_records_the_reason(self):
|
|
143
|
+
media = 'ppt/media/source.png'
|
|
144
|
+
package = _Archive(media, _png([(15, 80, 160, 255)]))
|
|
145
|
+
shape = {
|
|
146
|
+
'kind': 'pic',
|
|
147
|
+
'media': media,
|
|
148
|
+
'part': 'ppt/slides/slide2.xml',
|
|
149
|
+
'layer': 'slide',
|
|
150
|
+
'box_unrotated': {'x': 0, 'y': 0, 'w': 100, 'h': 100},
|
|
151
|
+
'box': {'x': 0, 'y': 0, 'w': 100, 'h': 100},
|
|
152
|
+
'rot': 90,
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
with tempfile.TemporaryDirectory() as outdir:
|
|
156
|
+
blocked = {}
|
|
157
|
+
with mock.patch.object(
|
|
158
|
+
extract, '_save_picture_instance_webp',
|
|
159
|
+
return_value='pillow-error: OSError: webp unavailable'):
|
|
160
|
+
rows = export_visible_picture_instances(
|
|
161
|
+
package, [shape], outdir, True, blocked)
|
|
162
|
+
images, _ = image_census([shape], [], Units(1920, 1080))
|
|
163
|
+
media_rows = export_media(package, images, outdir, True)
|
|
164
|
+
|
|
165
|
+
self.assertEqual([], rows)
|
|
166
|
+
self.assertEqual(media, shape['media'])
|
|
167
|
+
self.assertTrue(images[0]['visible_instance_blocked'])
|
|
168
|
+
self.assertTrue(media_rows[0]['candidate'])
|
|
169
|
+
self.assertIn('visible_instance_fallback', media_rows[0]['reasons'])
|
|
170
|
+
self.assertEqual({
|
|
171
|
+
media: {
|
|
172
|
+
'reason': 'pillow-error: OSError: webp unavailable',
|
|
173
|
+
'attempted_specs': 1,
|
|
174
|
+
'part_refs': ['ppt/slides/slide2.xml'],
|
|
175
|
+
},
|
|
176
|
+
}, blocked)
|
|
177
|
+
|
|
178
|
+
def test_alpha_modulations_combine_for_a_visible_instance(self):
|
|
179
|
+
media = 'ppt/media/source.png'
|
|
180
|
+
package = _Archive(media, _png([(20, 40, 60, 255)]))
|
|
181
|
+
picture_tree = ET.fromstring('''
|
|
182
|
+
<p:spTree xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
|
|
183
|
+
xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
|
|
184
|
+
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
|
|
185
|
+
<p:pic>
|
|
186
|
+
<p:nvPicPr><p:cNvPr id="1" name="half-alpha"/><p:cNvPicPr/><p:nvPr/></p:nvPicPr>
|
|
187
|
+
<p:blipFill>
|
|
188
|
+
<a:blip r:embed="rId1">
|
|
189
|
+
<a:alphaMod amt="50000"/>
|
|
190
|
+
<a:alphaModFix amt="50000"/>
|
|
191
|
+
</a:blip>
|
|
192
|
+
<a:stretch><a:fillRect/></a:stretch>
|
|
193
|
+
</p:blipFill>
|
|
194
|
+
<p:spPr>
|
|
195
|
+
<a:xfrm><a:off x="0" y="0"/><a:ext cx="100" cy="100"/></a:xfrm>
|
|
196
|
+
<a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
|
|
197
|
+
</p:spPr>
|
|
198
|
+
</p:pic>
|
|
199
|
+
</p:spTree>
|
|
200
|
+
''')
|
|
201
|
+
shapes = []
|
|
202
|
+
walk_tree(picture_tree, _ShapeContext(media), shapes)
|
|
203
|
+
|
|
204
|
+
self.assertEqual(shapes[0]['opacity'], 0.25)
|
|
205
|
+
with tempfile.TemporaryDirectory() as outdir:
|
|
206
|
+
rows = export_visible_picture_instances(package, shapes, outdir, True)
|
|
207
|
+
self.assertEqual(len(rows), 1)
|
|
208
|
+
rendered = Image.open(os.path.join(outdir, rows[0]['out'])).convert('RGBA')
|
|
209
|
+
self.assertAlmostEqual(rendered.getpixel((50, 50))[3], 64, delta=2)
|
|
210
|
+
|
|
211
|
+
def test_composites_full_canvas_gradient_and_solid_fill_overlays(self):
|
|
212
|
+
media = 'ppt/media/source.png'
|
|
213
|
+
slide = 'ppt/slides/slide1.xml'
|
|
214
|
+
package = _CompositeArchive(media, _png([(0, 0, 255, 255)]), slide)
|
|
215
|
+
picture = {
|
|
216
|
+
'part': slide,
|
|
217
|
+
'kind': 'pic',
|
|
218
|
+
'media': media,
|
|
219
|
+
'box_unrotated': {'x': 0, 'y': 0, 'w': 2, 'h': 1},
|
|
220
|
+
'w_pct': 100,
|
|
221
|
+
'h_pct': 100,
|
|
222
|
+
}
|
|
223
|
+
shape_base = {
|
|
224
|
+
'geom': {'prst': 'rect'},
|
|
225
|
+
'line': {'none': True},
|
|
226
|
+
}
|
|
227
|
+
gradient_overlay = {
|
|
228
|
+
**shape_base,
|
|
229
|
+
'part': slide,
|
|
230
|
+
'kind': 'sp',
|
|
231
|
+
'box_unrotated': {'x': 0, 'y': 0, 'w': 2, 'h': 1},
|
|
232
|
+
'w_pct': 100,
|
|
233
|
+
'h_pct': 100,
|
|
234
|
+
'fill': {
|
|
235
|
+
'type': 'gradient',
|
|
236
|
+
'stops': [
|
|
237
|
+
{'pos': 0, 'color': {'resolved': 'rgba(255,0,0,0.5)'}},
|
|
238
|
+
{'pos': 100, 'color': {'resolved': 'rgba(255,0,0,0.5)'}},
|
|
239
|
+
],
|
|
240
|
+
'angle_deg': 0,
|
|
241
|
+
},
|
|
242
|
+
}
|
|
243
|
+
solid_overlay = {
|
|
244
|
+
**shape_base,
|
|
245
|
+
'part': slide,
|
|
246
|
+
'kind': 'sp',
|
|
247
|
+
'box_unrotated': {'x': 0, 'y': 0, 'w': 2, 'h': 1},
|
|
248
|
+
'w_pct': 100,
|
|
249
|
+
'h_pct': 100,
|
|
250
|
+
'fill': {
|
|
251
|
+
'type': 'solid',
|
|
252
|
+
'color': {'resolved': 'rgba(0,255,0,0.5)'},
|
|
253
|
+
},
|
|
254
|
+
}
|
|
255
|
+
text_overlay = dict(solid_overlay, text={
|
|
256
|
+
'paragraphs': [{'runs': [{'text': 'Do not flatten me'}]}],
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
self.assertTrue(_is_full_canvas_fill_overlay(gradient_overlay))
|
|
260
|
+
self.assertFalse(_is_full_canvas_fill_overlay(text_overlay))
|
|
261
|
+
self.assertFalse(_is_full_canvas_fill_overlay({
|
|
262
|
+
**gradient_overlay,
|
|
263
|
+
'fill': {**gradient_overlay['fill'], 'path': 'circle'},
|
|
264
|
+
}))
|
|
265
|
+
with tempfile.TemporaryDirectory() as outdir:
|
|
266
|
+
part_map, rows, _ = compose_backgrounds(
|
|
267
|
+
package,
|
|
268
|
+
{'layout_of_slide': {}, 'master_of_layout': {}},
|
|
269
|
+
[picture, gradient_overlay, solid_overlay, text_overlay],
|
|
270
|
+
{},
|
|
271
|
+
Units(2, 1),
|
|
272
|
+
outdir,
|
|
273
|
+
True,
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
self.assertEqual(set(part_map), {slide})
|
|
277
|
+
self.assertEqual(len(rows), 1)
|
|
278
|
+
self.assertEqual(
|
|
279
|
+
[layer['kind'] for layer in rows[0]['composited_from']],
|
|
280
|
+
['picture', 'fill_overlay', 'fill_overlay'],
|
|
281
|
+
)
|
|
282
|
+
rendered = Image.open(os.path.join(outdir, rows[0]['out'])).convert('RGB')
|
|
283
|
+
red, green, blue = rendered.getpixel((0, 0))
|
|
284
|
+
self.assertGreater(red, 50)
|
|
285
|
+
self.assertGreater(green, 100)
|
|
286
|
+
self.assertGreater(blue, 50)
|
|
287
|
+
|
|
288
|
+
def test_preserves_fill_and_picture_draw_order(self):
|
|
289
|
+
media = 'ppt/media/source.png'
|
|
290
|
+
slide = 'ppt/slides/slide1.xml'
|
|
291
|
+
package = _CompositeArchive(media, _png([(0, 0, 255, 255)]), slide)
|
|
292
|
+
overlay = {
|
|
293
|
+
'part': slide,
|
|
294
|
+
'kind': 'sp',
|
|
295
|
+
'geom': {'prst': 'rect'},
|
|
296
|
+
'line': {'none': True},
|
|
297
|
+
'box_unrotated': {'x': 0, 'y': 0, 'w': 2, 'h': 1},
|
|
298
|
+
'w_pct': 100,
|
|
299
|
+
'h_pct': 100,
|
|
300
|
+
'fill': {'type': 'solid', 'color': {'resolved': 'rgba(255,0,0,0.5)'}},
|
|
301
|
+
}
|
|
302
|
+
picture = {
|
|
303
|
+
'part': slide,
|
|
304
|
+
'kind': 'pic',
|
|
305
|
+
'media': media,
|
|
306
|
+
'box_unrotated': {'x': 0, 'y': 0, 'w': 2, 'h': 1},
|
|
307
|
+
'w_pct': 100,
|
|
308
|
+
'h_pct': 100,
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
with tempfile.TemporaryDirectory() as outdir:
|
|
312
|
+
_, rows, _ = compose_backgrounds(
|
|
313
|
+
package,
|
|
314
|
+
{'layout_of_slide': {}, 'master_of_layout': {}},
|
|
315
|
+
[overlay, picture],
|
|
316
|
+
{},
|
|
317
|
+
Units(2, 1),
|
|
318
|
+
outdir,
|
|
319
|
+
True,
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
rendered = Image.open(os.path.join(outdir, rows[0]['out'])).convert('RGB')
|
|
323
|
+
self.assertEqual(rendered.getpixel((0, 0)), (0, 0, 255))
|
|
324
|
+
|
|
325
|
+
def test_path_gradient_background_is_not_linearized_for_compositing(self):
|
|
326
|
+
media = 'ppt/media/source.png'
|
|
327
|
+
slide = 'ppt/slides/slide1.xml'
|
|
328
|
+
package = _CompositeArchive(media, _png([(0, 0, 255, 255)]), slide)
|
|
329
|
+
picture = {
|
|
330
|
+
'part': slide,
|
|
331
|
+
'kind': 'pic',
|
|
332
|
+
'media': media,
|
|
333
|
+
'box_unrotated': {'x': 0, 'y': 0, 'w': 2, 'h': 1},
|
|
334
|
+
'w_pct': 100,
|
|
335
|
+
'h_pct': 100,
|
|
336
|
+
'rot': 90,
|
|
337
|
+
}
|
|
338
|
+
background = {
|
|
339
|
+
'type': 'gradient',
|
|
340
|
+
'path': 'circle',
|
|
341
|
+
'stops': [
|
|
342
|
+
{'pos': 0, 'color': {'resolved': '#FF0000'}},
|
|
343
|
+
{'pos': 100, 'color': {'resolved': '#00FF00'}},
|
|
344
|
+
],
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
self.assertIsNone(background_css(background))
|
|
348
|
+
with tempfile.TemporaryDirectory() as outdir:
|
|
349
|
+
_, rows, _ = compose_backgrounds(
|
|
350
|
+
package,
|
|
351
|
+
{'layout_of_slide': {}, 'master_of_layout': {}},
|
|
352
|
+
[picture],
|
|
353
|
+
{slide: background},
|
|
354
|
+
Units(2, 1),
|
|
355
|
+
outdir,
|
|
356
|
+
True,
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
rendered = Image.open(os.path.join(outdir, rows[0]['out'])).convert('RGB')
|
|
360
|
+
self.assertEqual(rendered.getpixel((0, 0)), (0, 0, 255))
|
|
361
|
+
|
|
55
362
|
|
|
56
363
|
if __name__ == '__main__':
|
|
57
364
|
unittest.main()
|
package/steering/design-html/skills/pptx-style-extract/scripts/test_design_consumer_contract.py
CHANGED
|
@@ -57,6 +57,11 @@ class DesignConsumerContractTest(unittest.TestCase):
|
|
|
57
57
|
self.assertIn('背景中已经可见的固定标题不再创建文本', body)
|
|
58
58
|
self.assertIn('没有 `subtitle` 槽就不新增副标题', body)
|
|
59
59
|
self.assertIn('区带自带 `margin: [左, 右]` 时用它的', body)
|
|
60
|
+
self.assertIn('必须完整阅读本 `design.md` 和 `layouts.md`', body)
|
|
61
|
+
self.assertIn('确认全部页型后再开始搭页', body)
|
|
62
|
+
self.assertIn('不能只看摘要、前几个页型', body)
|
|
63
|
+
self.assertIn('data-pptx-layout="<页型名>"', body)
|
|
64
|
+
self.assertIn('交付前据此核验 logo 只出现在该页型的资产槽中', body)
|
|
60
65
|
|
|
61
66
|
|
|
62
67
|
if __name__ == '__main__':
|
|
@@ -1087,6 +1087,104 @@ layouts:
|
|
|
1087
1087
|
self.assertIn('avoid: TODO禁放区列表', background_block)
|
|
1088
1088
|
self.assertIn('pairing_rule: "TODO', background_block)
|
|
1089
1089
|
|
|
1090
|
+
def test_background_controls_only_edit_existing_image_backgrounds(self):
|
|
1091
|
+
archetype = {
|
|
1092
|
+
'name': 'content',
|
|
1093
|
+
'zh': '内容页',
|
|
1094
|
+
'role': 'content',
|
|
1095
|
+
'pages': [1],
|
|
1096
|
+
'rep': 1,
|
|
1097
|
+
'bg': 'bg-content',
|
|
1098
|
+
'slots': [],
|
|
1099
|
+
'decor': [],
|
|
1100
|
+
'pic_n': 0,
|
|
1101
|
+
'confidence': 'high',
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
with tempfile.TemporaryDirectory() as output_dir:
|
|
1105
|
+
emit_layouts([archetype], output_dir)
|
|
1106
|
+
with open(os.path.join(output_dir, 'layout-controls.yaml'),
|
|
1107
|
+
encoding='utf-8') as stream:
|
|
1108
|
+
controls = stream.read()
|
|
1109
|
+
|
|
1110
|
+
self.assertIn('只编辑本草案已列出的真实图片背景', controls)
|
|
1111
|
+
self.assertIn('纯色、渐变、外框、几何装饰和透明叠层不新建 bg_rules', controls)
|
|
1112
|
+
self.assertIn('bg-content:', controls)
|
|
1113
|
+
|
|
1114
|
+
def test_template_layout_keeps_inherited_linear_gradient_background_as_decor(self):
|
|
1115
|
+
layout_part = 'ppt/slideLayouts/slideLayout1.xml'
|
|
1116
|
+
data = {
|
|
1117
|
+
'canvas': {'px': [1920, 1080]},
|
|
1118
|
+
'form_hint': {'form': 3},
|
|
1119
|
+
'layouts': [{
|
|
1120
|
+
'part': layout_part,
|
|
1121
|
+
'name': '内容',
|
|
1122
|
+
'used_by_slides': 1,
|
|
1123
|
+
'background': {
|
|
1124
|
+
'type': 'gradient',
|
|
1125
|
+
'stops': [
|
|
1126
|
+
{'pos': 0, 'color': {'resolved': '#4B5CF5'}},
|
|
1127
|
+
{'pos': 100, 'color': {'resolved': '#233AFB'}},
|
|
1128
|
+
],
|
|
1129
|
+
'angle_deg': 0,
|
|
1130
|
+
},
|
|
1131
|
+
}, {
|
|
1132
|
+
'part': 'ppt/slideLayouts/slideLayout2.xml',
|
|
1133
|
+
'name': '目录',
|
|
1134
|
+
'used_by_slides': 0,
|
|
1135
|
+
}, {
|
|
1136
|
+
'part': 'ppt/slideLayouts/slideLayout3.xml',
|
|
1137
|
+
'name': '封底',
|
|
1138
|
+
'used_by_slides': 0,
|
|
1139
|
+
}],
|
|
1140
|
+
'slides': [{
|
|
1141
|
+
'part': 'ppt/slides/slide1.xml',
|
|
1142
|
+
'layout': layout_part,
|
|
1143
|
+
'background': None,
|
|
1144
|
+
}],
|
|
1145
|
+
'images': [],
|
|
1146
|
+
'media': [],
|
|
1147
|
+
'theme_topology': {'themes': ['single'], 'per_master': [], 'default': 'single'},
|
|
1148
|
+
'reference_graph': {
|
|
1149
|
+
'master_of_layout': {},
|
|
1150
|
+
'layout_of_slide': {'ppt/slides/slide1.xml': layout_part},
|
|
1151
|
+
},
|
|
1152
|
+
'background_composites': {},
|
|
1153
|
+
}
|
|
1154
|
+
shapes = []
|
|
1155
|
+
for index, part in enumerate((layout_part,
|
|
1156
|
+
'ppt/slideLayouts/slideLayout2.xml',
|
|
1157
|
+
'ppt/slideLayouts/slideLayout3.xml'), 1):
|
|
1158
|
+
shapes.append({
|
|
1159
|
+
'part': part,
|
|
1160
|
+
'layer': 'layout',
|
|
1161
|
+
'kind': 'sp',
|
|
1162
|
+
'box': {'x': 100, 'y': 100, 'w': 600, 'h': 100},
|
|
1163
|
+
'ph': {'type': 'title', 'idx': str(index)},
|
|
1164
|
+
'text': {'paragraphs': [{'runs': [{'text': 'Title %d' % index,
|
|
1165
|
+
'sz_px': 40}]}]},
|
|
1166
|
+
})
|
|
1167
|
+
|
|
1168
|
+
with tempfile.TemporaryDirectory() as output_dir:
|
|
1169
|
+
os.makedirs(os.path.join(output_dir, 'ref'))
|
|
1170
|
+
with open(os.path.join(output_dir, 'ref', 'shapes.json'), 'w',
|
|
1171
|
+
encoding='utf-8') as stream:
|
|
1172
|
+
import json
|
|
1173
|
+
json.dump({'shapes': shapes}, stream)
|
|
1174
|
+
archetypes, _, _ = draft_layouts(data, output_dir)
|
|
1175
|
+
emit_layouts(archetypes, output_dir)
|
|
1176
|
+
emit_manifest(data, [], [], output_dir, archetypes)
|
|
1177
|
+
with open(os.path.join(output_dir, 'layouts.yaml'), encoding='utf-8') as stream:
|
|
1178
|
+
layouts_yaml = stream.read()
|
|
1179
|
+
with open(os.path.join(output_dir, 'manifest.yaml'), encoding='utf-8') as stream:
|
|
1180
|
+
manifest = stream.read()
|
|
1181
|
+
|
|
1182
|
+
self.assertIn('box: [0, 0, 1920, 1080]', layouts_yaml)
|
|
1183
|
+
self.assertIn('background-image: linear-gradient(90deg, #4B5CF5 0%, #233AFB 100%)',
|
|
1184
|
+
layouts_yaml)
|
|
1185
|
+
self.assertIn('value: "[0, 0, 1920, 1080]"', manifest)
|
|
1186
|
+
self.assertIn('reason: "PPT 背景铺满画布"', manifest)
|
|
1187
|
+
|
|
1090
1188
|
def test_asset_vision_groups_fail_when_page_screenshot_is_unreadable(self):
|
|
1091
1189
|
candidates = [{
|
|
1092
1190
|
'id': 'asset-1',
|
|
@@ -1221,6 +1319,10 @@ layouts:
|
|
|
1221
1319
|
'fullscreen': True,
|
|
1222
1320
|
'probe': {'alpha_mean': 5, 'near_blank': True},
|
|
1223
1321
|
}))
|
|
1322
|
+
self.assertTrue(needs_asset_judgment({
|
|
1323
|
+
'fullscreen': False,
|
|
1324
|
+
'probe': {'alpha_mean': 5, 'near_blank': True},
|
|
1325
|
+
}))
|
|
1224
1326
|
|
|
1225
1327
|
def test_shape_fill_images_are_exposed_as_slide_marks(self):
|
|
1226
1328
|
data = {
|