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

1from PIL import Image 

2from .. import objects 

3from .. import NVector, Color 

4from ..utils.color import from_uint8 

5 

6 

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 

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 

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 

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 

151def pixel_add_layer_paths(animation, raster): 

152 return animation.add_layer(pixel_to_layer_paths(raster)) 

153 

154 

155def 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 

216def _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 

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 

265 

266 return _vectorizing_func(filenames, frame_delay, framerate, callback) 

267 

268 

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) 

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 

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 

293 

294 return _vectorizing_func(filenames, frame_delay, framerate, callback) 

295 

296 

297def 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)