python-lottie  0.6.11+devcecd248
A framework to work with lottie files and telegram animated stickers (tgs)
pixel.py
Go to the documentation of this file.
1 from PIL import Image
2 from .. import objects
3 from .. import NVector, Color
4 from ..utils.color import from_uint8
5 
6 
7 class 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
17 
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()
26 
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()
35 
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()
47 
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)
52 
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)
67 
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)
73 
74 
75 def 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
82 
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  )
89 
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))
100 
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
107 
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
118 
119  g = groups.setdefault(colort, set())
120  g.add(gen.to_shape(scale))
121 
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
134 
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
147 
148  return layer
149 
150 
151 def pixel_add_layer_paths(animation, raster):
152  return animation.add_layer(pixel_to_layer_paths(raster))
153 
154 
155 def pixel_add_layer_rects(animation, raster):
156  layer = animation.add_layer(objects.ShapeLayer())
157  last_rects = {}
158  groups = {}
159 
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
168 
169  def group(colort):
170  return groups.setdefault(colort, set())
171 
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
203 
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
214 
215 
216 def _vectorizing_func(filenames, frame_delay, framerate, callback):
217  if not isinstance(filenames, list):
218  filenames = [filenames]
219 
220  animation = objects.Animation(0, framerate)
221  nframes = 0
222  time = 0
223 
224  for filename in filenames:
225  if isinstance(filename, Image.Image):
226  raster = filename
227  else:
228  raster = Image.open(filename)
229 
230  if nframes == 0:
231  animation.width = raster.width
232  animation.height = raster.height
233 
234  if not hasattr(raster, "is_animated"):
235  raster.n_frames = 1
236  raster.seek = lambda x: None
237 
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
250 
251  animation.out_point = time
252  return animation
253 
254 
255 def 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
265 
266  return _vectorizing_func(filenames, frame_delay, framerate, callback)
267 
268 
269 def 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)
274 
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
281 
282  return animation
283 
284 
285 def 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
293 
294  return _vectorizing_func(filenames, frame_delay, framerate, callback)
295 
296 
297 def pixel_to_animation_paths(filenames, frame_delay=1, framerate=60):
298  """!
299  @brief Converts pixel art to vector paths
300 
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
309 
310  return _vectorizing_func(filenames, frame_delay, framerate, callback)
lottie.objects.shapes.Group
ShapeElement that can contain other shapes.
Definition: shapes.py:430
lottie.parsers.pixel.Polygen.vertices
vertices
Definition: pixel.py:9
lottie.parsers.pixel.pixel_to_animation
def pixel_to_animation(filenames, frame_delay=1, framerate=60)
Converts pixel art to vector.
Definition: pixel.py:285
lottie.objects.shapes.Stroke
Solid stroke.
Definition: shapes.py:648
lottie.objects.bezier.Bezier
Single bezier curve.
Definition: bezier.py:123
lottie.objects.layers.ImageLayer
Definition: layers.py:218
lottie.objects.shapes.Path
Animatable Bezier curve.
Definition: shapes.py:398
lottie.objects.animation.Animation
Top level object, describing the animation.
Definition: animation.py:62
lottie.parsers.pixel.pixel_add_layer_rects
def pixel_add_layer_rects(animation, raster)
Definition: pixel.py:155
lottie.parsers.pixel.Polygen.to_shape
def to_shape(self, scale)
Definition: pixel.py:53
lottie.objects.shapes.Rect
A simple rectangle shape.
Definition: shapes.py:149
lottie.parsers.pixel.Polygen.add_pixel_x_neg
def add_pixel_x_neg(self, x, y)
Definition: pixel.py:27
lottie.parsers.pixel.pixel_add_layer_paths
def pixel_add_layer_paths(animation, raster)
Definition: pixel.py:151
lottie.objects.shapes.Fill
Solid fill color.
Definition: shapes.py:506
lottie.parsers.pixel.pixel_to_animation_paths
def pixel_to_animation_paths(filenames, frame_delay=1, framerate=60)
Converts pixel art to vector paths.
Definition: pixel.py:297
lottie.parsers.pixel.raster_to_linked_assets
def raster_to_linked_assets(filenames, frame_delay=1, framerate=60)
Loads external assets.
Definition: pixel.py:269
lottie.parsers.pixel.Polygen._has_x
_has_x
Definition: pixel.py:15
lottie.parsers.pixel.Polygen._to_rect
def _to_rect(self, id1, id2, scale)
Definition: pixel.py:48
lottie.parsers.pixel.Polygen.add_pixel_y
def add_pixel_y(self, x, y)
Definition: pixel.py:36
lottie.parsers.pixel.Polygen.add_pixel_x
def add_pixel_x(self, x, y)
Definition: pixel.py:18
lottie.parsers.pixel.Polygen
Definition: pixel.py:7
lottie.parsers.pixel.Polygen._has_y
_has_y
Definition: pixel.py:16
lottie.parsers.pixel.raster_to_embedded_assets
def raster_to_embedded_assets(filenames, frame_delay=1, framerate=60, embed_format=None)
Loads external assets.
Definition: pixel.py:255
lottie.parsers.pixel.pixel_to_layer_paths
def pixel_to_layer_paths(raster, scale=1, stroke_width=None)
Definition: pixel.py:75
lottie.parsers.pixel.Polygen.__init__
def __init__(self, x, y)
Definition: pixel.py:8
lottie.utils.color.from_uint8
def from_uint8(r, g, b, a=255)
Definition: color.py:7
lottie.objects.layers.ShapeLayer
Layer containing ShapeElement objects.
Definition: layers.py:192