python-lottie  0.6.11+deved4e6c7
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.verticesvertices = [
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_has_x = False
16  self._has_y_has_y = False
17 
18  def add_pixel_x(self, x, y):
19  i = self.verticesvertices.index(NVector(x, y))
20  if len(self.verticesvertices) > i and self.verticesvertices[i+1] == NVector(x, y+1):
21  self._has_x_has_x = True
22  self.verticesvertices.insert(i+1, NVector(x+1, y))
23  self.verticesvertices.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.verticesvertices.index(NVector(x+1, y))
29  if i > 0 and self.verticesvertices[i-1] == NVector(x+1, y+1):
30  self._has_x_has_x = True
31  self.verticesvertices.insert(i, NVector(x, y))
32  self.verticesvertices.insert(i, NVector(x, y+1))
33  else:
34  raise ValueError()
35 
36  def add_pixel_y(self, x, y):
37  i = self.verticesvertices.index(NVector(x, y))
38  if i > 0 and self.verticesvertices[i-1] == NVector(x+1, y):
39  self._has_y_has_y = True
40  if i > 1 and self.verticesvertices[i-2] == NVector(x+1, y+1):
41  self.verticesvertices[i-1] = NVector(x, y+1)
42  else:
43  self.verticesvertices.insert(i, NVector(x, y+1))
44  self.verticesvertices.insert(i, NVector(x+1, y+1))
45  else:
46  raise ValueError()
47 
48  def _to_rect(self, id1, id2, scale):
49  p1 = self.verticesvertices[id1] * scale
50  p2 = self.verticesvertices[id2] * scale
51  return objects.Rect((p1+p2)/2, p2-p1)
52 
53  def to_shape(self, scale):
54  if not self._has_x_has_x or not self._has_y_has_y:
55  return self._to_rect_to_rect(0, int(len(self.verticesvertices)/2), scale)
56  bez = objects.Bezier()
57  bez.closed = True
58  for point in self.verticesvertices:
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)
Top level object, describing the animation.
Definition: animation.py:62
Single bezier curve.
Definition: bezier.py:123
Layer containing ShapeElement objects.
Definition: layers.py:193
Solid fill color.
Definition: shapes.py:509
ShapeElement that can contain other shapes.
Definition: shapes.py:433
Animatable Bezier curve.
Definition: shapes.py:399
A simple rectangle shape.
Definition: shapes.py:150
def _to_rect(self, id1, id2, scale)
Definition: pixel.py:48
def add_pixel_x(self, x, y)
Definition: pixel.py:18
def add_pixel_y(self, x, y)
Definition: pixel.py:36
def to_shape(self, scale)
Definition: pixel.py:53
def add_pixel_x_neg(self, x, y)
Definition: pixel.py:27
def __init__(self, x, y)
Definition: pixel.py:8
def pixel_to_animation_paths(filenames, frame_delay=1, framerate=60)
Converts pixel art to vector paths.
Definition: pixel.py:297
def pixel_to_animation(filenames, frame_delay=1, framerate=60)
Converts pixel art to vector.
Definition: pixel.py:285
def pixel_add_layer_rects(animation, raster)
Definition: pixel.py:155
def raster_to_linked_assets(filenames, frame_delay=1, framerate=60)
Loads external assets.
Definition: pixel.py:269
def pixel_to_layer_paths(raster, scale=1, stroke_width=None)
Definition: pixel.py:75
def raster_to_embedded_assets(filenames, frame_delay=1, framerate=60, embed_format=None)
Loads external assets.
Definition: pixel.py:255
def pixel_add_layer_paths(animation, raster)
Definition: pixel.py:151
def from_uint8(r, g, b, a=255)
Definition: color.py:7