sticker-convert 2.1.6__py3-none-any.whl → 2.1.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. sticker_convert/__init__.py +1 -1
  2. sticker_convert/__main__.py +7 -4
  3. sticker_convert/cli.py +39 -31
  4. sticker_convert/converter.py +432 -0
  5. sticker_convert/downloaders/download_base.py +40 -16
  6. sticker_convert/downloaders/download_kakao.py +103 -136
  7. sticker_convert/downloaders/download_line.py +16 -6
  8. sticker_convert/downloaders/download_signal.py +48 -32
  9. sticker_convert/downloaders/download_telegram.py +71 -26
  10. sticker_convert/gui.py +78 -129
  11. sticker_convert/{gui_frames → gui_components/frames}/comp_frame.py +2 -3
  12. sticker_convert/{gui_frames → gui_components/frames}/config_frame.py +3 -4
  13. sticker_convert/{gui_frames → gui_components/frames}/control_frame.py +2 -2
  14. sticker_convert/{gui_frames → gui_components/frames}/cred_frame.py +4 -4
  15. sticker_convert/{gui_frames → gui_components/frames}/input_frame.py +4 -4
  16. sticker_convert/{gui_frames → gui_components/frames}/output_frame.py +3 -3
  17. sticker_convert/{gui_frames → gui_components/frames}/progress_frame.py +1 -1
  18. sticker_convert/{utils → gui_components}/gui_utils.py +38 -21
  19. sticker_convert/{gui_windows → gui_components/windows}/advanced_compression_window.py +3 -2
  20. sticker_convert/{gui_windows → gui_components/windows}/base_window.py +3 -2
  21. sticker_convert/{gui_windows → gui_components/windows}/kakao_get_auth_window.py +3 -3
  22. sticker_convert/{gui_windows → gui_components/windows}/line_get_auth_window.py +2 -2
  23. sticker_convert/{gui_windows → gui_components/windows}/signal_get_auth_window.py +2 -2
  24. sticker_convert/{flow.py → job.py} +91 -102
  25. sticker_convert/job_option.py +301 -0
  26. sticker_convert/resources/compression.json +1 -1
  27. sticker_convert/uploaders/compress_wastickers.py +95 -74
  28. sticker_convert/uploaders/upload_base.py +16 -4
  29. sticker_convert/uploaders/upload_signal.py +100 -62
  30. sticker_convert/uploaders/upload_telegram.py +168 -128
  31. sticker_convert/uploaders/xcode_imessage.py +202 -132
  32. sticker_convert/{auth → utils/auth}/get_kakao_auth.py +7 -5
  33. sticker_convert/{auth → utils/auth}/get_line_auth.py +3 -3
  34. sticker_convert/{auth → utils/auth}/get_signal_auth.py +1 -1
  35. sticker_convert/utils/fake_cb_msg.py +5 -2
  36. sticker_convert/utils/{cache_store.py → files/cache_store.py} +7 -3
  37. sticker_convert/utils/files/dir_utils.py +64 -0
  38. sticker_convert/utils/{json_manager.py → files/json_manager.py} +5 -4
  39. sticker_convert/utils/files/metadata_handler.py +226 -0
  40. sticker_convert/utils/files/run_bin.py +58 -0
  41. sticker_convert/utils/{apple_png_normalize.py → media/apple_png_normalize.py} +23 -20
  42. sticker_convert/utils/{codec_info.py → media/codec_info.py} +41 -35
  43. sticker_convert/utils/media/decrypt_kakao.py +68 -0
  44. sticker_convert/utils/media/format_verify.py +184 -0
  45. sticker_convert/utils/url_detect.py +16 -14
  46. {sticker_convert-2.1.6.dist-info → sticker_convert-2.1.7.dist-info}/METADATA +8 -9
  47. {sticker_convert-2.1.6.dist-info → sticker_convert-2.1.7.dist-info}/RECORD +52 -50
  48. {sticker_convert-2.1.6.dist-info → sticker_convert-2.1.7.dist-info}/WHEEL +1 -1
  49. sticker_convert/utils/converter.py +0 -407
  50. sticker_convert/utils/curr_dir.py +0 -70
  51. sticker_convert/utils/format_verify.py +0 -188
  52. sticker_convert/utils/metadata_handler.py +0 -190
  53. sticker_convert/utils/run_bin.py +0 -46
  54. /sticker_convert/{gui_frames → gui_components/frames}/right_clicker.py +0 -0
  55. {sticker_convert-2.1.6.dist-info → sticker_convert-2.1.7.dist-info}/LICENSE +0 -0
  56. {sticker_convert-2.1.6.dist-info → sticker_convert-2.1.7.dist-info}/entry_points.txt +0 -0
  57. {sticker_convert-2.1.6.dist-info → sticker_convert-2.1.7.dist-info}/top_level.txt +0 -0
@@ -1,407 +0,0 @@
1
- #!/usr/bin/env python3
2
- import os
3
- import io
4
- from multiprocessing.queues import Queue as QueueType
5
- from typing import Optional, Union
6
-
7
- import imageio.v3 as iio
8
- from rlottie_python import LottieAnimation # type: ignore
9
- from apngasm_python._apngasm_python import APNGAsm, create_frame_from_rgba
10
- import numpy as np
11
- from PIL import Image
12
- import av # type: ignore
13
- from av.codec.context import CodecContext # type: ignore
14
- import webp # type: ignore
15
- import oxipng
16
-
17
- from .codec_info import CodecInfo # type: ignore
18
- from .cache_store import CacheStore # type: ignore
19
- from .format_verify import FormatVerify # type: ignore
20
- from .fake_cb_msg import FakeCbMsg # type: ignore
21
-
22
- def get_step_value(max: int, min: int, step: int, steps: int) -> Optional[int]:
23
- if max and min:
24
- return round((max - min) * step / steps + min)
25
- else:
26
- return None
27
-
28
- class StickerConvert:
29
- def __init__(self, in_f: Union[str, list[str, io.BytesIO]], out_f: str, opt_comp: dict, cb_msg=print):
30
- if type(cb_msg) != QueueType:
31
- cb_msg = FakeCbMsg(cb_msg)
32
-
33
- if type(in_f) == str:
34
- self.in_f = in_f
35
- self.in_f_name = os.path.split(self.in_f)[1]
36
- self.in_f_ext = CodecInfo.get_file_ext(self.in_f)
37
- else:
38
- self.in_f = in_f[1]
39
- self.in_f_name = os.path.split(in_f[0])[1]
40
- self.in_f_ext = CodecInfo.get_file_ext(in_f[0])
41
-
42
- self.out_f = out_f
43
- self.out_f_name = os.path.split(self.out_f)[1]
44
- if os.path.splitext(out_f)[0] not in ('null', 'bytes'):
45
- self.out_f_ext = CodecInfo.get_file_ext(self.out_f)
46
- else:
47
- self.out_f_ext = os.path.splitext(out_f)[1]
48
-
49
- self.cb_msg = cb_msg
50
- self.frames_raw: list[np.ndarray] = []
51
- self.frames_processed: list[np.ndarray] = []
52
- self.opt_comp = opt_comp
53
- self.preset = opt_comp.get('preset')
54
-
55
- self.size_max = opt_comp.get('size_max') if type(opt_comp.get('size_max')) == int else None
56
- self.size_max_img = opt_comp.get('size_max', {}).get('img') if not self.size_max else self.size_max
57
- self.size_max_vid = opt_comp.get('size_max', {}).get('vid') if not self.size_max else self.size_max
58
-
59
- self.format = opt_comp.get('format') if type(opt_comp.get('format')) == str else None
60
- self.format_img = opt_comp.get('format', {}).get('img') if not self.format else self.format
61
- self.format_vid = opt_comp.get('format', {}).get('vid') if not self.format else self.format
62
-
63
- self.fps = opt_comp.get('fps') if type(opt_comp.get('fps')) == int else None
64
- self.fps_min = opt_comp.get('fps', {}).get('min') if not self.fps else self.fps
65
- self.fps_max = opt_comp.get('fps', {}).get('max') if not self.fps else self.fps
66
-
67
- self.res_w = opt_comp.get('res', {}).get('w') if type(opt_comp.get('res', {}).get('w')) == int else None
68
- self.res_w_min = opt_comp.get('res', {}).get('w', {}).get('min') if not self.res_w else self.res_w
69
- self.res_w_max = opt_comp.get('res', {}).get('w', {}).get('max') if not self.res_w else self.res_w
70
-
71
- self.res_h = opt_comp.get('res', {}).get('h') if type(opt_comp.get('res', {}).get('h')) == int else None
72
- self.res_h_min = opt_comp.get('res', {}).get('h', {}).get('min') if not self.res_h else self.res_h
73
- self.res_h_max = opt_comp.get('res', {}).get('h', {}).get('max') if not self.res_h else self.res_h
74
-
75
- self.quality = opt_comp.get('quality') if type(opt_comp.get('quality')) == int else None
76
- self.quality_min = opt_comp.get('quality', {}).get('min') if not self.quality else self.quality
77
- self.quality_max = opt_comp.get('quality', {}).get('max') if not self.quality else self.quality
78
-
79
- self.color = opt_comp.get('color') if type(opt_comp.get('color')) == int else None
80
- self.color_min = opt_comp.get('color', {}).get('min') if not self.color else self.color
81
- self.color_max = opt_comp.get('color', {}).get('max') if not self.color else self.color
82
-
83
- self.duration = opt_comp.get('duration') if type(opt_comp.get('duration')) == int else None
84
- self.duration_min = opt_comp.get('duration', {}).get('min') if not self.duration else self.duration
85
- self.duration_max = opt_comp.get('duration', {}).get('max') if not self.duration else self.duration
86
-
87
- if not self.size_max and not self.size_max_img and not self.size_max_vid:
88
- self.steps = 1
89
- else:
90
- self.steps = opt_comp.get('steps') if opt_comp.get('steps') else 1
91
- self.fake_vid = opt_comp.get('fake_vid')
92
- self.cache_dir = opt_comp.get('cache_dir')
93
-
94
- self.tmp_f = None
95
- self.tmp_fs: list[bytes] = []
96
-
97
- self.apngasm = APNGAsm() # type: ignore[call-arg]
98
-
99
- def convert(self) -> tuple[bool, str, Union[None, bytes, str], int]:
100
- if (FormatVerify.check_format(self.in_f, format=self.out_f_ext) and
101
- FormatVerify.check_file_res(self.in_f, res=self.opt_comp.get('res')) and
102
- FormatVerify.check_file_fps(self.in_f, fps=self.opt_comp.get('fps')) and
103
- FormatVerify.check_file_size(self.in_f, size=self.opt_comp.get('size_max')) and
104
- FormatVerify.check_duration(self.in_f, duration=self.opt_comp.get('duration'))):
105
- self.cb_msg.put(f'[S] Compatible file found, skip compress and just copy {self.in_f_name} -> {self.out_f_name}')
106
-
107
- with open(self.in_f, 'rb') as f:
108
- self.write_out(f.read())
109
- return True, self.in_f, self.out_f, os.path.getsize(self.in_f)
110
-
111
- self.cb_msg.put(f'[I] Start compressing {self.in_f_name} -> {self.out_f_name}')
112
-
113
- steps_list = []
114
- for step in range(self.steps, -1, -1):
115
- steps_list.append((
116
- get_step_value(self.res_w_max, self.res_w_min, step, self.steps),
117
- get_step_value(self.res_h_max, self.res_h_min, step, self.steps),
118
- get_step_value(self.quality_max, self.quality_min, step, self.steps),
119
- get_step_value(self.fps_max, self.fps_min, step, self.steps),
120
- get_step_value(self.color_max, self.color_min, step, self.steps)
121
- ))
122
- self.tmp_fs = [None] * (self.steps + 1)
123
-
124
- step_lower = 0
125
- step_upper = self.steps
126
-
127
- if self.size_max_vid == None and self.size_max_img == None:
128
- # No limit to size, create the best quality result
129
- step_current = 0
130
- else:
131
- step_current = round((step_lower + step_upper) / 2)
132
-
133
- self.frames_import()
134
- while True:
135
- param = steps_list[step_current]
136
- self.res_w = param[0]
137
- self.res_h = param[1]
138
- self.quality = param[2]
139
- self.fps = param[3]
140
- self.color = param[4]
141
-
142
- self.tmp_f = io.BytesIO()
143
- self.cb_msg.put(f'[C] Compressing {self.in_f_name} -> {self.out_f_name} res={self.res_w}x{self.res_h}, quality={self.quality}, fps={self.fps}, color={self.color} (step {step_lower}-{step_current}-{step_upper})')
144
-
145
- self.frames_processed = self.frames_drop(self.frames_raw)
146
- self.frames_processed = self.frames_resize(self.frames_processed)
147
- self.frames_export()
148
-
149
- self.tmp_f.seek(0)
150
- size = self.tmp_f.getbuffer().nbytes
151
- if CodecInfo.is_anim(self.in_f):
152
- size_max = self.size_max_vid
153
- else:
154
- size_max = self.size_max_img
155
-
156
- if not size_max or size < size_max:
157
- self.tmp_fs[step_current] = self.tmp_f.read()
158
-
159
- for i in range(self.steps+1):
160
- if self.tmp_fs[i] != None:
161
- self.tmp_fs[min(i+2,self.steps+1):] = [None] * (self.steps+1 - min(i+2,self.steps+1))
162
- break
163
-
164
- if not size_max:
165
- self.write_out(self.tmp_fs[step_current], step_current)
166
- return True, self.in_f, self.out_f, size
167
-
168
- if size < size_max:
169
- if step_upper - step_lower > 1:
170
- step_upper = step_current
171
- step_current = int((step_lower + step_upper) / 2)
172
- self.cb_msg.put(f'[<] Compressed {self.in_f_name} -> {self.out_f_name} but size {size} < limit {size_max}, recompressing')
173
- else:
174
- self.write_out(self.tmp_fs[step_current], step_current)
175
- return True, self.in_f, self.out_f, size
176
- else:
177
- if step_upper - step_lower > 1:
178
- step_lower = step_current
179
- step_current = round((step_lower + step_upper) / 2)
180
- self.cb_msg.put(f'[>] Compressed {self.in_f_name} -> {self.out_f_name} but size {size} > limit {size_max}, recompressing')
181
- else:
182
- if self.steps - step_current > 1:
183
- self.write_out(self.tmp_fs[step_current + 1], step_current)
184
- return True, self.in_f, self.out_f, size
185
- else:
186
- self.cb_msg.put(f'[F] Failed Compression {self.in_f_name} -> {self.out_f_name}, cannot get below limit {size_max} with lowest quality under current settings')
187
- return False, self.in_f, self.out_f, size
188
-
189
- def write_out(self, data: bytes, step_current: Optional[int] = None):
190
- if os.path.splitext(self.out_f)[0] == 'none':
191
- self.out_f = None
192
- elif os.path.splitext(self.out_f)[0] == 'bytes':
193
- self.out_f = data
194
- else:
195
- with open(self.out_f, 'wb+') as f:
196
- f.write(data)
197
-
198
- if step_current:
199
- self.cb_msg.put(f'[S] Successful compression {self.in_f_name} -> {self.out_f_name} (step {step_current})')
200
-
201
- def frames_import(self):
202
- if self.in_f_ext in ('.tgs', '.lottie', '.json'):
203
- self.frames_import_lottie()
204
- else:
205
- self.frames_import_imageio()
206
-
207
- def frames_import_imageio(self):
208
- if self.in_f_ext in '.webp':
209
- # ffmpeg do not support webp decoding (yet)
210
- for frame in iio.imiter(self.in_f, plugin='pillow', mode='RGBA'):
211
- self.frames_raw.append(frame)
212
- else:
213
- frame_format = 'rgba'
214
- # Crashes when handling some webm in yuv420p and convert to rgba
215
- # https://github.com/PyAV-Org/PyAV/issues/1166
216
- metadata = iio.immeta(self.in_f, plugin='pyav', exclude_applied=False)
217
- context = None
218
- if metadata.get('video_format') == 'yuv420p':
219
- if metadata.get('alpha_mode') != '1':
220
- frame_format = 'rgb24'
221
- if metadata.get('codec') == 'vp8':
222
- context = CodecContext.create('v8', 'r')
223
- elif metadata.get('codec') == 'vp9':
224
- context = CodecContext.create('libvpx-vp9', 'r')
225
-
226
- with av.open(self.in_f) as container:
227
- if not context:
228
- context = container.streams.video[0].codec_context
229
- for packet in container.demux(video=0):
230
- for frame in context.decode(packet):
231
- frame = frame.to_ndarray(format=frame_format)
232
- if frame_format == 'rgb24':
233
- frame = np.dstack((frame, np.zeros(frame.shape[:2], dtype=np.uint8)+255))
234
- self.frames_raw.append(frame)
235
-
236
- def frames_import_lottie(self):
237
- if self.in_f_ext == '.tgs':
238
- anim = LottieAnimation.from_tgs(self.in_f)
239
- else:
240
- if type(self.in_f) == str:
241
- anim = LottieAnimation.from_file(self.in_f)
242
- else:
243
- anim = LottieAnimation.from_data(self.in_f.read().decode('utf-8'))
244
-
245
- for i in range(anim.lottie_animation_get_totalframe()):
246
- frame = np.asarray(anim.render_pillow_frame(frame_num=i))
247
- self.frames_raw.append(frame)
248
-
249
- anim.lottie_animation_destroy()
250
-
251
- def frames_resize(self, frames_in: list[np.ndarray]) -> list[np.ndarray]:
252
- frames_out = []
253
-
254
- for frame in frames_in:
255
- im = Image.fromarray(frame, 'RGBA')
256
- width, height = im.size
257
-
258
- if self.res_w == None:
259
- self.res_w = width
260
- if self.res_h == None:
261
- self.res_h = height
262
-
263
- if width > height:
264
- width_new = self.res_w
265
- height_new = height * self.res_w // width
266
- else:
267
- height_new = self.res_h
268
- width_new = width * self.res_h // height
269
- im = im.resize((width_new, height_new), resample=Image.LANCZOS)
270
- im_new = Image.new('RGBA', (self.res_w, self.res_h), (0, 0, 0, 0))
271
- im_new.paste(im, ((self.res_w - width_new) // 2, (self.res_h - height_new) // 2))
272
- frames_out.append(np.asarray(im_new))
273
-
274
- return frames_out
275
-
276
- def frames_drop(self, frames_in: list[np.ndarray]) -> list[np.ndarray]:
277
- if not self.fps:
278
- return [frames_in[0]]
279
-
280
- frames_out = []
281
-
282
- frames_orig = CodecInfo.get_file_frames(self.in_f)
283
- fps_orig = CodecInfo.get_file_fps(self.in_f)
284
- duration_orig = frames_orig / fps_orig * 1000
285
-
286
- # fps_ratio: 1 frame in new anim equal to how many frame in old anim
287
- # speed_ratio: How much to speed up / slow down
288
- fps_ratio = fps_orig / self.fps
289
- if self.duration_min and self.duration_min > 0 and duration_orig < self.duration_min:
290
- speed_ratio = duration_orig / self.duration_min
291
- elif self.duration_max and self.duration_max > 0 and duration_orig > self.duration_max:
292
- speed_ratio = duration_orig / self.duration_max
293
- else:
294
- speed_ratio = 1
295
-
296
- frame_current = 0
297
- frame_current_float = 0
298
- while frame_current < len(frames_in):
299
- frames_out.append(frames_in[frame_current])
300
- frame_current_float += fps_ratio * speed_ratio
301
- frame_current = round(frame_current_float)
302
-
303
- return frames_out
304
-
305
- def frames_export(self):
306
- if self.out_f_ext in ('.apng', '.png') and self.fps:
307
- self.frames_export_apng()
308
- elif self.out_f_ext == '.png':
309
- self.frames_export_png()
310
- elif self.out_f_ext == '.webp' and self.fps:
311
- self.frames_export_webp()
312
- elif self.fps:
313
- self.frames_export_pyav()
314
- else:
315
- self.frames_export_pil()
316
-
317
- def frames_export_pil(self):
318
- image = Image.fromarray(self.frames_processed[0])
319
- image.save(
320
- self.tmp_f,
321
- format=self.out_f_ext.replace('.', ''),
322
- quality=self.quality
323
- )
324
-
325
- def frames_export_pyav(self):
326
- options = {}
327
-
328
- if type(self.quality) == int:
329
- options['quality'] = str(self.quality)
330
- options['lossless'] = '0'
331
-
332
- if self.out_f_ext == '.gif':
333
- codec = 'gif'
334
- pixel_format = 'rgb8'
335
- options['loop'] = '0'
336
- elif self.out_f_ext in ('.apng', '.png'):
337
- codec = 'apng'
338
- pixel_format = 'rgba'
339
- options['plays'] = '0'
340
- else:
341
- codec = 'vp9'
342
- pixel_format = 'yuva420p'
343
- options['loop'] = '0'
344
-
345
- with av.open(self.tmp_f, 'w', format=self.out_f_ext.replace('.', '')) as output:
346
- out_stream = output.add_stream(codec, rate=self.fps, options=options)
347
- out_stream.width = self.res_w
348
- out_stream.height = self.res_h
349
- out_stream.pix_fmt = pixel_format
350
-
351
- for frame in self.frames_processed:
352
- av_frame = av.VideoFrame.from_ndarray(frame, format='rgba')
353
- for packet in out_stream.encode(av_frame):
354
- output.mux(packet)
355
-
356
- for packet in out_stream.encode():
357
- output.mux(packet)
358
-
359
- def frames_export_webp(self):
360
- config = webp.WebPConfig.new(quality=self.quality)
361
- enc = webp.WebPAnimEncoder.new(self.res_w, self.res_h)
362
- timestamp_ms = 0
363
- for frame in self.frames_processed:
364
- pic = webp.WebPPicture.from_numpy(frame)
365
- enc.encode_frame(pic, timestamp_ms, config=config)
366
- timestamp_ms += int(1 / self.fps * 1000)
367
- anim_data = enc.assemble(timestamp_ms)
368
- self.tmp_f.write(anim_data.buffer())
369
-
370
- def frames_export_png(self):
371
- image = Image.fromarray(self.frames_processed[0], 'RGBA')
372
- if self.color and self.color < 256:
373
- image_quant = image.quantize(colors=self.color, method=2)
374
- else:
375
- image_quant = image
376
- with io.BytesIO() as f:
377
- image_quant.save(f, format='png')
378
- f.seek(0)
379
- frame_optimized = oxipng.optimize_from_memory(f.read(), level=4)
380
- self.tmp_f.write(frame_optimized)
381
-
382
- def frames_export_apng(self):
383
- frames_concat = np.concatenate(self.frames_processed)
384
- image_concat = Image.fromarray(frames_concat, 'RGBA')
385
- if self.color and self.color < 256:
386
- image_quant = image_concat.quantize(colors=self.color, method=2)
387
- else:
388
- image_quant = image_concat
389
-
390
- for i in range(0, image_quant.height, self.res_h):
391
- with io.BytesIO() as f:
392
- image_quant.crop((0, i, image_quant.width, i+self.res_h)).save(f, format='png')
393
- f.seek(0)
394
- frame_optimized = oxipng.optimize_from_memory(f.read(), level=4)
395
- image_final = Image.open(io.BytesIO(frame_optimized)).convert('RGBA')
396
- frame_final = create_frame_from_rgba(np.array(image_final), image_final.width, image_final.height)
397
- frame_final.delay_num = int(1000 / self.fps)
398
- frame_final.delay_den = 1000
399
- self.apngasm.add_frame(frame_final)
400
-
401
- with CacheStore.get_cache_store(path=self.cache_dir) as tempdir:
402
- self.apngasm.assemble(os.path.join(tempdir, f'out{self.out_f_ext}'))
403
-
404
- with open(os.path.join(tempdir, f'out{self.out_f_ext}'), 'rb') as f:
405
- self.tmp_f.write(f.read())
406
-
407
- self.apngasm.reset()
@@ -1,70 +0,0 @@
1
- #!/usr/bin/env python3
2
- import os
3
- import sys
4
- import platform
5
-
6
- class CurrDir:
7
- @staticmethod
8
- def get_curr_dir():
9
- appimage_path = os.getenv('APPIMAGE')
10
-
11
- cwd = os.getcwd()
12
- home_dir = os.path.expanduser('~')
13
- if os.path.isdir(os.path.join(home_dir, 'Desktop')):
14
- fallback_dir = os.path.join(home_dir, 'Desktop')
15
- else:
16
- fallback_dir = home_dir
17
-
18
- if platform.system() == 'Darwin' and getattr(sys, 'frozen', False) and '.app/Contents/MacOS' in cwd:
19
- if cwd.startswith('/Applications/'):
20
- curr_dir = fallback_dir
21
- else:
22
- curr_dir = os.path.abspath('../../../')
23
- elif appimage_path:
24
- curr_dir = os.path.split(appimage_path)[0]
25
- elif (cwd.startswith('/usr/bin/')
26
- or cwd.startswith('/bin')
27
- or cwd.startswith('/usr/local/bin')
28
- or cwd.startswith('C:\\Program Files')
29
- or 'site-packages' in __file__):
30
- curr_dir = fallback_dir
31
- else:
32
- curr_dir = cwd
33
-
34
- if os.access(curr_dir, os.W_OK):
35
- return curr_dir
36
- else:
37
- return fallback_dir
38
-
39
- @staticmethod
40
- def get_config_dir():
41
- appimage_path = os.getenv('APPIMAGE')
42
-
43
- cwd = os.getcwd()
44
- if platform.system() == 'Windows':
45
- fallback_dir = os.path.expandvars('%APPDATA%\\sticker-convert')
46
- else:
47
- fallback_dir = os.path.expanduser('~/.config/sticker-convert')
48
-
49
- if platform.system() == 'Darwin' and getattr(sys, 'frozen', False) and '.app/Contents/MacOS' in cwd:
50
- if cwd.startswith('/Applications/'):
51
- config_dir = fallback_dir
52
- else:
53
- config_dir = os.path.abspath('../../../')
54
- elif appimage_path:
55
- config_dir = os.path.split(appimage_path)[0]
56
- elif (cwd.startswith('/usr/bin/')
57
- or cwd.startswith('/bin')
58
- or cwd.startswith('/usr/local/bin')
59
- or cwd.startswith('C:\\Program Files')
60
- or 'site-packages' in __file__):
61
- config_dir = fallback_dir
62
- else:
63
- config_dir = cwd
64
-
65
- os.makedirs(config_dir, exist_ok=True)
66
- if os.access(config_dir, os.W_OK):
67
- return config_dir
68
- else:
69
- os.makedirs(fallback_dir, exist_ok=True)
70
- return fallback_dir
@@ -1,188 +0,0 @@
1
- #!/usr/bin/env python3
2
- import os
3
- import unicodedata
4
- import re
5
- from typing import Optional, Union
6
-
7
- from .codec_info import CodecInfo # type: ignore
8
-
9
- '''
10
- Example of spec
11
- spec = {
12
- "res": {
13
- "w": {
14
- "min": 256,
15
- "max": 512
16
- },
17
- "h": {
18
- "min": 256,
19
- "max": 512
20
- }
21
- },
22
- "fps": {
23
- "min": 1,
24
- "max": 30
25
- },
26
- "size_max": {
27
- "vid": 500000,
28
- "img": 300000
29
- },
30
- "duration": {
31
- "min": 0,
32
- "max": 3
33
- },
34
- "animated": True,
35
- "square": True,
36
- "format": ".apng"
37
- }
38
- '''
39
-
40
- class FormatVerify:
41
- @staticmethod
42
- def check_file(file: str, spec: dict) -> bool:
43
- return (
44
- FormatVerify.check_presence(file) and
45
- FormatVerify.check_file_res(file, res=spec.get('res'), square=spec.get('square')) and
46
- FormatVerify.check_file_fps(file, fps=spec.get('fps')) and
47
- FormatVerify.check_file_size(file, size=spec.get('size_max')) and
48
- FormatVerify.check_animated(file, animated=spec.get('animated')) and
49
- FormatVerify.check_format(file, format=spec.get('format')) and
50
- FormatVerify.check_duration(file, duration=spec.get('duration'))
51
- )
52
-
53
- @staticmethod
54
- def check_presence(file: str) -> bool:
55
- return os.path.isfile(file)
56
-
57
- @staticmethod
58
- def check_file_res(file: str, res: Optional[dict[dict, int]] = None, square: Optional[bool] = None) -> bool:
59
- file_width, file_height = CodecInfo.get_file_res(file)
60
-
61
- if res and (res.get('w', {}).get('min') and res.get('w', {}).get('max')) and (file_width < res['w']['min'] or file_width > res['w']['max']): # type: ignore[call-overload,index]
62
- return False
63
- if res and (res.get('h', {}).get('min') and res.get('h', {}).get('max')) and (file_height < res['h']['min'] or file_height > res['h']['max']): # type: ignore[call-overload,index]
64
- return False
65
- if square != None and file_height != file_width:
66
- return False
67
-
68
- return True
69
-
70
- @staticmethod
71
- def check_file_fps(file: str, fps: Optional[dict[str, float]]) -> bool:
72
- file_fps = CodecInfo.get_file_fps(file)
73
-
74
- if fps and fps.get('min') != None and file_fps < fps['min']:
75
- return False
76
- if fps and fps.get('max') != None and file_fps > fps['max']:
77
- return False
78
-
79
- return True
80
-
81
- @staticmethod
82
- def check_file_size(file: str, size: Optional[dict[str, int]] = None) -> bool:
83
- file_size = os.path.getsize(file)
84
- file_animated = CodecInfo.is_anim(file)
85
-
86
- if file_animated == True and size and size.get('vid') != None and file_size > size['vid']:
87
- return False
88
- if file_animated == False and size and size.get('img') != None and file_size > size['img']:
89
- return False
90
-
91
- return True
92
-
93
- @staticmethod
94
- def check_animated(file: str, animated: Optional[bool] = None) -> bool:
95
- if animated != None and CodecInfo.is_anim(file) != animated:
96
- return False
97
-
98
- return True
99
-
100
- @staticmethod
101
- def check_format(file: str, format: Union[dict[str, str], str, tuple, list, None] = None, allow_compat_ext: bool = True):
102
- compat_ext = {
103
- '.jpg': '.jpeg',
104
- '.jpeg': '.jpg',
105
- '.png': '.apng',
106
- '.apng': '.png'
107
- }
108
-
109
- formats = []
110
- if format != None:
111
- if type(format) == dict:
112
- if FormatVerify.check_animated(file):
113
- format = format.get('vid')
114
- else:
115
- format = format.get('img')
116
-
117
- if type(format) == str:
118
- formats.append(format)
119
- elif (type(format) == tuple or type(format) == list):
120
- formats.extend(format)
121
-
122
- if allow_compat_ext:
123
- for fmt in formats.copy():
124
- if fmt in compat_ext:
125
- formats.append(compat_ext.get(fmt)) # type: ignore[arg-type]
126
- if CodecInfo.get_file_ext(file) not in formats:
127
- return False
128
-
129
- return True
130
-
131
- @staticmethod
132
- def check_duration(file: str, duration: Optional[dict[str, int]] = None) -> bool:
133
- file_duration = CodecInfo.get_file_duration(file)
134
- if duration and duration.get('min') != None and file_duration < duration['min']:
135
- return False
136
- if duration and duration.get('max') != None and file_duration > duration['max']:
137
- return False
138
-
139
- return True
140
-
141
- @staticmethod
142
- def sanitize_filename(filename: str) -> str:
143
- # Based on https://gitlab.com/jplusplus/sanitize-filename/-/blob/master/sanitize_filename/sanitize_filename.py
144
- # Replace illegal character with '_'
145
- """Return a fairly safe version of the filename.
146
-
147
- We don't limit ourselves to ascii, because we want to keep municipality
148
- names, etc, but we do want to get rid of anything potentially harmful,
149
- and make sure we do not exceed Windows filename length limits.
150
- Hence a less safe blacklist, rather than a whitelist.
151
- """
152
- blacklist = ["\\", "/", ":", "*", "?", "\"", "<", ">", "|", "\0"]
153
- reserved = [
154
- "CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5",
155
- "COM6", "COM7", "COM8", "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5",
156
- "LPT6", "LPT7", "LPT8", "LPT9",
157
- ] # Reserved words on Windows
158
- filename = "".join(c if c not in blacklist else '_' for c in filename)
159
- # Remove all charcters below code point 32
160
- filename = "".join(c if 31 < ord(c) else '_' for c in filename)
161
- filename = unicodedata.normalize("NFKD", filename)
162
- filename = filename.rstrip(". ") # Windows does not allow these at end
163
- filename = filename.strip()
164
- if all([x == "." for x in filename]):
165
- filename = "__" + filename
166
- if filename in reserved:
167
- filename = "__" + filename
168
- if len(filename) == 0:
169
- filename = "__"
170
- if len(filename) > 255:
171
- parts = re.split(r"/|\\", filename)[-1].split(".")
172
- if len(parts) > 1:
173
- ext = "." + parts.pop()
174
- filename = filename[:-len(ext)]
175
- else:
176
- ext = ""
177
- if filename == "":
178
- filename = "__"
179
- if len(ext) > 254:
180
- ext = ext[254:]
181
- maxl = 255 - len(ext)
182
- filename = filename[:maxl]
183
- filename = filename + ext
184
- # Re-check last character (if there was no extension)
185
- filename = filename.rstrip(". ")
186
- if len(filename) == 0:
187
- filename = "__"
188
- return filename