-
Notifications
You must be signed in to change notification settings - Fork 16
/
core.py
223 lines (192 loc) · 8.68 KB
/
core.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
# -*- coding: utf-8 -*-
from __future__ import absolute_import, unicode_literals
from logging import getLogger
import psd_tools
from psd_tools import BBox
from psd_tools.constants import TaggedBlock
from psd_tools.user_api.psd_image import _VisibleLayer
from psd2svg.converter.constants import BLEND_MODE
from psd2svg.utils.xml import safe_utf8
logger = getLogger(__name__)
class LayerConverter(object):
def _add_group(self, layers):
for layer in reversed(layers):
self._add_layer(layer)
for clip in reversed(layer.clip_layers):
self._add_layer(clip)
def _add_layer(self, layer):
target = self._get_target(layer)
if target is None:
return
target.get_iri() # Assign id
mask = self._add_mask_if_exist(layer)
effects = self._get_effects(layer)
vector_stroke = self._get_vector_stroke(layer, target)
logger.debug('{} {} {}'.format(layer.name, target, mask))
if mask:
target['mask'] = mask.get_funciri()
# Blending options.
if not layer._info.flags.visible:
target['visibility'] = 'hidden'
if vector_stroke:
vector_stroke['visibility'] = 'hidden'
blend_mode = BLEND_MODE.get(layer._info.blend_mode, 'normal')
if not blend_mode == 'normal':
target['style'] = 'mix-blend-mode: {}'.format(blend_mode)
opacity, fill_opacity = self._get_opacity(layer)
if opacity < 1.0:
target['opacity'] = opacity
if not effects and fill_opacity < 1.0:
target['opacity'] = opacity * fill_opacity
# TODO: Filter to strokes requires a different approach.
blocks = layer._tagged_blocks
if effects:
interior_blend_mode = None
if blocks.get(b'infx', False):
interior_blend_mode = blend_mode
target = self._add_effects(effects, layer, target, fill_opacity,
interior_blend_mode)
if layer._info.clipping and self._clip_group:
self._clip_group.add(target)
# Acutally clipping with mask and mix-blend-mode does not
# correctly blend in SVG. clipPath does, but then cannot use
# bitmap for clipping. Any workaround?
self._clip_group['style'] = 'mix-blend-mode: {}'.format(
blend_mode)
if not self._clbl:
self._clip_group.attribs['style'] += '; isolation: isolate;'
elif layer._info.clipping:
# Convert the last target to a clipping mask
last_stroke = None
last_target = self._current_group.elements[-1]
if 'vector-stroke' in last_target.attribs.get(
'class', '').split():
last_stroke = self._current_group.elements.pop()
last_target = self._current_group.elements[-1]
mask = self._dwg.defs.add(self._dwg.mask())
mask_bbox = layer.bbox
mask.add(self._dwg.use(last_target.get_iri()))
mask['color-interpolation'] = 'sRGB'
self._clip_group = self._dwg.g(mask=mask.get_funciri())
self._clip_group['class'] = 'clipping-mask'
self._current_group.add(self._clip_group)
self._clip_group.add(target)
# Acutally clipping with mask and mix-blend-mode does not
# correctly blend in SVG. clipPath does, but then cannot use
# bitmap for clipping. Any workaround?
self._clip_group['style'] = 'mix-blend-mode: {}'.format(
blend_mode)
if not self._clbl:
self._clip_group.attribs['style'] += '; isolation: isolate;'
if last_stroke: # Move the stroke to the front.
self._current_group.add(last_stroke)
else:
self._clip_group = None
self._current_group.add(target)
# Keep info for when next layer is clip group
self._clbl = blocks.get(b'clbl')
if vector_stroke:
self._current_group.add(vector_stroke)
def _get_target(self, layer):
target = None
if isinstance(layer, psd_tools.Group):
# Group.
current_group = self._current_group
target = self._dwg.g()
target.set_desc(title=safe_utf8(layer.name))
target['class'] = 'manual-group'
self._current_group = target
self._add_group(layer.layers)
self._current_group = current_group
elif isinstance(layer, _VisibleLayer) and (
layer.bbox.width > 0 and layer.bbox.height > 0):
# Regular pixel layer.
target = self._dwg.image(
self._get_image_href(layer.as_PIL()),
insert=(layer.bbox.x1, layer.bbox.y1),
size=(layer.bbox.width, layer.bbox.height),
debug=False) # To disable attribute validation.
target.set_desc(title=safe_utf8(layer.name))
text = self._get_text(layer)
if text:
if self.text_mode == 'text-only':
text.set_desc(title=safe_utf8(layer.name))
target = text
elif self.text_mode == 'image-only':
pass
else:
target['class'] = 'text-image'
container = self._dwg.g()
container.elements.append(target.elements.pop(0))
container.add(target)
container.add(text)
container['class'] = 'text-container'
target = container
elif layer.kind == 'shape':
blocks = layer._tagged_blocks
vsms = blocks.get(b'vsms', blocks.get(b'vmsk'))
anchors = [
(p['anchor'][1] * self.width,
p['anchor'][0] * self.height)
for p in vsms.path if p['selector'] in (1, 2, 4, 5)]
fill = self._get_fill(layer)
target = self._dwg.polygon(points=anchors, fill=fill)
target.set_desc(title=safe_utf8(layer.name))
elif any(TaggedBlock.is_fill_key(key) for key in layer._tagged_blocks.keys()):
record = layer._info
bbox = BBox(record.left, record.top, record.right, record.bottom)
target = self._dwg.rect(
insert=(bbox.x1, bbox.y1), size=(bbox.width, bbox.height),
fill=self._get_fill(layer))
target.set_desc(title=safe_utf8(layer.name))
else:
target = self._get_adjustments(layer)
if target:
logger.debug('{} {}'.format(layer.name, target))
else:
logger.warning('Not rendered: {}'.format(layer))
return None
return target
def _add_mask_if_exist(self, layer):
mask_data = layer.mask
if not mask_data or not mask_data.is_valid or \
mask_data.mask_data.flags.mask_disabled:
return None
background_color = mask_data.mask_data.real_background_color
if background_color is None:
background_color = mask_data.background_color
# In SVG, mask needs a default rect.
default_bbox = layer.bbox
if not default_bbox:
default_bbox = psd_tools.BBox(0, 0, self.width, self.height)
mask = self._dwg.defs.add(self._dwg.mask(
size=(default_bbox.width, default_bbox.height)))
mask.add(self._dwg.rect(
insert=(default_bbox.x1, default_bbox.y1),
size=(default_bbox.width, default_bbox.height),
fill='rgb({0},{0},{0})'.format(background_color)))
bbox = mask_data.bbox
mask.add(self._dwg.image(
self._get_image_href(mask_data.as_PIL()),
size=(bbox.width, bbox.height),
insert=(bbox.x1, bbox.y1)))
mask['color-interpolation'] = 'sRGB'
return mask
def _get_opacity(self, layer):
record = layer._info
opacity = record.opacity / 255.0
fill_opacity = dict(record.tagged_blocks).get(b'iOpa', 255) / 255.0
return opacity, fill_opacity
def _add_photoshop_view(self):
# Embed original photoshop view.
image = self._psd.as_PIL()
original = self._dwg.add(self._dwg.image(
self._get_image_href(image), insert=(0, 0),
size=(self.width, self.height), visibility='hidden'))
original['class'] = 'photoshop-image'
def _has_visible_pixels(record):
return (record.bottom - record.top > 0 and
record.right - record.left > 0)
def _is_shape_layer(record):
blocks = dict(record.tagged_blocks)
return (b'vmsk' in blocks or b'vsms' in blocks) and b'vogk' in blocks