Coverage for lib/lottie/parsers/pixel.py: 1%
217 statements
« prev ^ index » next coverage.py v7.2.2, created at 2023-03-20 16:17 +0100
« prev ^ index » next coverage.py v7.2.2, created at 2023-03-20 16:17 +0100
1from PIL import Image
2from .. import objects
3from .. import NVector, Color
4from ..utils.color import from_uint8
7class Polygen:
8 def __init__(self, x, y):
9 self.vertices = [
10 NVector(x, y),
11 NVector(x+1, y),
12 NVector(x+1, y+1),
13 NVector(x, y+1),
14 ]
15 self._has_x = False
16 self._has_y = False
18 def add_pixel_x(self, x, y):
19 i = self.vertices.index(NVector(x, y))
20 if len(self.vertices) > i and self.vertices[i+1] == NVector(x, y+1):
21 self._has_x = True
22 self.vertices.insert(i+1, NVector(x+1, y))
23 self.vertices.insert(i+2, NVector(x+1, y+1))
24 else:
25 raise ValueError()
27 def add_pixel_x_neg(self, x, y):
28 i = self.vertices.index(NVector(x+1, y))
29 if i > 0 and self.vertices[i-1] == NVector(x+1, y+1):
30 self._has_x = True
31 self.vertices.insert(i, NVector(x, y))
32 self.vertices.insert(i, NVector(x, y+1))
33 else:
34 raise ValueError()
36 def add_pixel_y(self, x, y):
37 i = self.vertices.index(NVector(x, y))
38 if i > 0 and self.vertices[i-1] == NVector(x+1, y):
39 self._has_y = True
40 if i > 1 and self.vertices[i-2] == NVector(x+1, y+1):
41 self.vertices[i-1] = NVector(x, y+1)
42 else:
43 self.vertices.insert(i, NVector(x, y+1))
44 self.vertices.insert(i, NVector(x+1, y+1))
45 else:
46 raise ValueError()
48 def _to_rect(self, id1, id2, scale):
49 p1 = self.vertices[id1] * scale
50 p2 = self.vertices[id2] * scale
51 return objects.Rect((p1+p2)/2, p2-p1)
53 def to_shape(self, scale):
54 if not self._has_x or not self._has_y:
55 return self._to_rect(0, int(len(self.vertices)/2), scale)
56 bez = objects.Bezier()
57 bez.closed = True
58 for point in self.vertices:
59 point = point * scale
60 if len(bez.vertices) > 1 and (
61 bez.vertices[-1].x == bez.vertices[-2].x == point.x or
62 bez.vertices[-1].y == bez.vertices[-2].y == point.y
63 ):
64 bez.vertices[-1] = point
65 else:
66 bez.add_point(point)
68 if len(bez.vertices) > 2 and bez.vertices[0].x == bez.vertices[-1].x == bez.vertices[-2].x:
69 bez.vertices.pop()
70 bez.out_tangents.pop()
71 bez.in_tangents.pop()
72 return objects.Path(bez)
75def pixel_to_layer_paths(raster, scale=1, stroke_width=None):
76 layer = objects.ShapeLayer()
77 groups = {}
78 processed = set()
79 xneg_candidates = set()
80 if stroke_width is None:
81 stroke_width = 0.1 * scale
83 def avail(x, y):
84 rid = (x, y)
85 return not (
86 x < 0 or x >= raster.width or y >= raster.height or
87 rid in processed or raster.getpixel(rid) != colort
88 )
90 def recurse(gen, x, y, xneg):
91 processed.add((x, y))
92 if avail(x+1, y):
93 gen.add_pixel_x(x+1, y)
94 recurse(gen, x+1, y, False)
95 if avail(x, y+1):
96 gen.add_pixel_y(x, y+1)
97 recurse(gen, x, y+1, True)
98 if xneg and avail(x-1, y):
99 xneg_candidates.add((x-1, y))
101 for y in range(raster.height):
102 for x in range(raster.width):
103 pid = (x, y)
104 colort = raster.getpixel(pid)
105 if colort[-1] == 0 or pid in processed:
106 continue
108 gen = Polygen(x, y)
109 xneg_candidates = set()
110 recurse(gen, x, y, False)
111 xneg_candidates -= processed
112 while xneg_candidates:
113 p = next(iter(sorted(xneg_candidates, key=lambda t: (t[1], t[0]))))
114 gen.add_pixel_x_neg(*p)
115 recurse(gen, p[0], p[1], True)
116 processed.add(p)
117 xneg_candidates -= processed
119 g = groups.setdefault(colort, set())
120 g.add(gen.to_shape(scale))
122 # Debug
123 #for colort, rects in groups.items():
124 #for rect in rects:
125 #g = layer.add_shape(objects.Group())
126 #g.shapes = [rect] + g.shapes
127 #g.name = "".join("%02x" % c for c in colort)
128 #color = from_uint8(*colort[:3])
129 #opacity = colort[-1] / 255 * 100
130 #stroke = g.add_shape(objects.Stroke(NVector(0, 0, 0), stroke_width))
131 #fill = g.add_shape(objects.Fill())
132 #fill.color.value = color
133 #fill.opacity.value = 20
135 for colort, rects in groups.items():
136 g = layer.add_shape(objects.Group())
137 g.shapes = list(rects) + g.shapes
138 g.name = "".join("%02x" % c for c in colort)
139 color = from_uint8(*colort[:3])
140 opacity = colort[-1] / 255 * 100
141 fill = g.add_shape(objects.Fill())
142 fill.color.value = color
143 fill.opacity.value = opacity
144 if stroke_width > 0:
145 stroke = g.add_shape(objects.Stroke(color, stroke_width))
146 stroke.opacity.value = opacity
148 return layer
151def pixel_add_layer_paths(animation, raster):
152 return animation.add_layer(pixel_to_layer_paths(raster))
155def pixel_add_layer_rects(animation, raster):
156 layer = animation.add_layer(objects.ShapeLayer())
157 last_rects = {}
158 groups = {}
160 def merge_up():
161 if last_rect and last_rect._start in last_rects:
162 yrect = last_rects[last_rect._start]
163 if yrect.size.value.x == last_rect.size.value.x and yrect._color == last_rect._color:
164 groups[last_rect._color].remove(last_rect)
165 yrect.position.value.y += 0.5
166 yrect.size.value.y += 1
167 rects[last_rect._start] = yrect
169 def group(colort):
170 return groups.setdefault(colort, set())
172 for y in range(raster.height):
173 rects = {}
174 last_color = None
175 last_rect = None
176 for x in range(raster.width):
177 colort = raster.getpixel((x, y))
178 if colort[-1] == 0:
179 last_color = 0
180 continue
181 yrect = last_rects.get(x, None)
182 if colort == last_color:
183 last_rect.position.value.x += 0.5
184 last_rect.size.value.x += 1
185 elif yrect and colort == yrect._color and yrect.size.value.x == 1:
186 yrect.position.value.y += 0.5
187 yrect.size.value.y += 1
188 rects[x] = yrect
189 last_color = last_rect = colort = None
190 else:
191 merge_up()
192 g = group(colort)
193 last_rect = objects.Rect()
194 g.add(last_rect)
195 last_rect.size.value = NVector(1, 1)
196 last_rect.position.value = NVector(x + 0.5, y + 0.5)
197 rects[x] = last_rect
198 last_rect._start = x
199 last_rect._color = colort
200 last_color = colort
201 merge_up()
202 last_rects = rects
204 for colort, rects in groups.items():
205 g = layer.add_shape(objects.Group())
206 g.shapes = list(rects) + g.shapes
207 g.name = "".join("%02x" % c for c in colort)
208 fill = g.add_shape(objects.Fill())
209 fill.color.value = from_uint8(*colort[:3])
210 fill.opacity.value = colort[-1] / 255 * 100
211 stroke = g.add_shape(objects.Stroke(fill.color.value, 0.1))
212 stroke.opacity.value = fill.opacity.value
213 return layer
216def _vectorizing_func(filenames, frame_delay, framerate, callback):
217 if not isinstance(filenames, list):
218 filenames = [filenames]
220 animation = objects.Animation(0, framerate)
221 nframes = 0
222 time = 0
224 for filename in filenames:
225 if isinstance(filename, Image.Image):
226 raster = filename
227 else:
228 raster = Image.open(filename)
230 if nframes == 0:
231 animation.width = raster.width
232 animation.height = raster.height
234 if not hasattr(raster, "is_animated"):
235 raster.n_frames = 1
236 raster.seek = lambda x: None
238 for frame in range(raster.n_frames):
239 raster.seek(frame)
240 new_im = Image.new("RGBA", raster.size)
241 new_im.paste(raster)
242 duration = frame_delay
243 image_duration = raster.info.get("duration", 0)
244 if image_duration:
245 duration = framerate * image_duration / 1000
246 callback(animation, new_im, nframes + frame, time, duration)
247 time += duration
248 new_im.close()
249 nframes += raster.n_frames
251 animation.out_point = time
252 return animation
255def raster_to_embedded_assets(filenames, frame_delay=1, framerate=60, embed_format=None):
256 """!
257 @brief Loads external assets
258 """
259 def callback(animation, raster, frame, time, duration):
260 asset = objects.assets.Image.embedded(raster, embed_format)
261 animation.assets.append(asset)
262 layer = animation.add_layer(objects.ImageLayer(asset.id))
263 layer.in_point = time
264 layer.out_point = layer.in_point + duration
266 return _vectorizing_func(filenames, frame_delay, framerate, callback)
269def raster_to_linked_assets(filenames, frame_delay=1, framerate=60):
270 """!
271 @brief Loads external assets
272 """
273 animation = objects.Animation(frame_delay * len(filenames), framerate)
275 for frame, filename in enumerate(filenames):
276 asset = objects.assets.Image.linked(filename)
277 animation.assets.append(asset)
278 layer = animation.add_layer(objects.ImageLayer(asset.id))
279 layer.in_point = frame * frame_delay
280 layer.out_point = layer.in_point + frame_delay
282 return animation
285def pixel_to_animation(filenames, frame_delay=1, framerate=60):
286 """!
287 @brief Converts pixel art to vector
288 """
289 def callback(animation, raster, frame, time, duration):
290 layer = pixel_add_layer_rects(animation, raster.convert("RGBA"))
291 layer.in_point = time
292 layer.out_point = layer.in_point + duration
294 return _vectorizing_func(filenames, frame_delay, framerate, callback)
297def pixel_to_animation_paths(filenames, frame_delay=1, framerate=60):
298 """!
299 @brief Converts pixel art to vector paths
301 Slower and yields larger files compared to pixel_to_animation,
302 but it produces a single shape for each area with the same color.
303 Mostly useful when you want to add your own animations to the loaded image
304 """
305 def callback(animation, raster, frame, time, duration):
306 layer = pixel_add_layer_paths(animation, raster.convert("RGBA"))
307 layer.in_point = time
308 layer.out_point = layer.in_point + duration
310 return _vectorizing_func(filenames, frame_delay, framerate, callback)