sticker-convert 2.7.13__tar.gz → 2.8.0__tar.gz

Sign up to get free protection for your applications and to get access to all the features.
Files changed (110) hide show
  1. {sticker-convert-2.7.13/src/sticker_convert.egg-info → sticker-convert-2.8.0}/PKG-INFO +1 -1
  2. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/pyproject.toml +1 -1
  3. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/converter.py +62 -36
  4. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/utils/media/codec_info.py +117 -37
  5. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/version.py +1 -1
  6. {sticker-convert-2.7.13 → sticker-convert-2.8.0/src/sticker_convert.egg-info}/PKG-INFO +1 -1
  7. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/LICENSE +0 -0
  8. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/MANIFEST.in +0 -0
  9. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/README.md +0 -0
  10. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/requirements.txt +0 -0
  11. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/setup.cfg +0 -0
  12. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/__init__.py +0 -0
  13. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/__main__.py +0 -0
  14. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/cli.py +0 -0
  15. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/definitions.py +0 -0
  16. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/downloaders/__init__.py +0 -0
  17. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/downloaders/download_base.py +0 -0
  18. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/downloaders/download_kakao.py +0 -0
  19. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/downloaders/download_line.py +0 -0
  20. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/downloaders/download_signal.py +0 -0
  21. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/downloaders/download_telegram.py +0 -0
  22. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/gui.py +0 -0
  23. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/gui_components/__init__.py +0 -0
  24. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/gui_components/frames/__init__.py +0 -0
  25. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/gui_components/frames/comp_frame.py +0 -0
  26. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/gui_components/frames/config_frame.py +0 -0
  27. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/gui_components/frames/control_frame.py +0 -0
  28. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/gui_components/frames/cred_frame.py +0 -0
  29. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/gui_components/frames/input_frame.py +0 -0
  30. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/gui_components/frames/output_frame.py +0 -0
  31. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/gui_components/frames/progress_frame.py +0 -0
  32. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/gui_components/frames/right_clicker.py +0 -0
  33. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/gui_components/gui_utils.py +0 -0
  34. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/gui_components/windows/__init__.py +0 -0
  35. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/gui_components/windows/advanced_compression_window.py +0 -0
  36. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/gui_components/windows/base_window.py +0 -0
  37. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/gui_components/windows/kakao_get_auth_window.py +0 -0
  38. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/gui_components/windows/line_get_auth_window.py +0 -0
  39. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/gui_components/windows/signal_get_auth_window.py +0 -0
  40. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/ios-message-stickers-template/.github/FUNDING.yml +0 -0
  41. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/ios-message-stickers-template/.gitignore +0 -0
  42. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/ios-message-stickers-template/README.md +0 -0
  43. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/ios-message-stickers-template/stickers/Info.plist +0 -0
  44. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Info.plist +0 -0
  45. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/Contents.json +0 -0
  46. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/Sticker Pack.stickerpack/Contents.json +0 -0
  47. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/Sticker Pack.stickerpack/Sticker 1.sticker/Contents.json +0 -0
  48. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/Sticker Pack.stickerpack/Sticker 1.sticker/Sticker 1.png +0 -0
  49. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/Sticker Pack.stickerpack/Sticker 2.sticker/Contents.json +0 -0
  50. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/Sticker Pack.stickerpack/Sticker 2.sticker/Sticker 2.png +0 -0
  51. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/Sticker Pack.stickerpack/Sticker 3.sticker/Contents.json +0 -0
  52. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/Sticker Pack.stickerpack/Sticker 3.sticker/Sticker 3.png +0 -0
  53. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/iMessage App Icon.stickersiconset/App-Store-1024x1024pt.png +0 -0
  54. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/iMessage App Icon.stickersiconset/Contents.json +0 -0
  55. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/iMessage App Icon.stickersiconset/Messages-App-Store-1024x768pt.png +0 -0
  56. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/iMessage App Icon.stickersiconset/Messages-iPad-67x50pt@2x.png +0 -0
  57. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/iMessage App Icon.stickersiconset/Messages-iPad-Pro-74x55pt@2x.png +0 -0
  58. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/iMessage App Icon.stickersiconset/Messages-iPhone-60x45pt@2x.png +0 -0
  59. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/iMessage App Icon.stickersiconset/Messages-iPhone-60x45pt@3x.png +0 -0
  60. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/iMessage App Icon.stickersiconset/Messages27x20pt@2x.png +0 -0
  61. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/iMessage App Icon.stickersiconset/Messages27x20pt@3x.png +0 -0
  62. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/iMessage App Icon.stickersiconset/Messages32x24pt@2x.png +0 -0
  63. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/iMessage App Icon.stickersiconset/Messages32x24pt@3x.png +0 -0
  64. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/iMessage App Icon.stickersiconset/iPad-Settings-29pt@2x.png +0 -0
  65. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/iMessage App Icon.stickersiconset/iPhone-Settings-29pt@3x.png +0 -0
  66. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/iMessage App Icon.stickersiconset/iPhone-settings-29pt@2x.png +0 -0
  67. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/ios-message-stickers-template/stickers.xcodeproj/project.pbxproj +0 -0
  68. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/ios-message-stickers-template/stickers.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -0
  69. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/ios-message-stickers-template/stickers.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +0 -0
  70. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/ios-message-stickers-template/stickers.xcodeproj/project.xcworkspace/xcuserdata/niklaspeterson.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
  71. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/ios-message-stickers-template/stickers.xcodeproj/xcuserdata/niklaspeterson.xcuserdatad/xcschemes/xcschememanagement.plist +0 -0
  72. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/job.py +0 -0
  73. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/job_option.py +0 -0
  74. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/resources/NotoColorEmoji.ttf +0 -0
  75. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/resources/appicon.icns +0 -0
  76. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/resources/appicon.ico +0 -0
  77. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/resources/appicon.png +0 -0
  78. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/resources/compression.json +0 -0
  79. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/resources/emoji.json +0 -0
  80. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/resources/help.json +0 -0
  81. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/resources/input.json +0 -0
  82. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/resources/output.json +0 -0
  83. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/uploaders/__init__.py +0 -0
  84. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/uploaders/compress_wastickers.py +0 -0
  85. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/uploaders/upload_base.py +0 -0
  86. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/uploaders/upload_signal.py +0 -0
  87. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/uploaders/upload_telegram.py +0 -0
  88. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/uploaders/xcode_imessage.py +0 -0
  89. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/utils/auth/get_kakao_auth.py +0 -0
  90. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/utils/auth/get_line_auth.py +0 -0
  91. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/utils/auth/get_signal_auth.py +0 -0
  92. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/utils/callback.py +0 -0
  93. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/utils/files/cache_store.py +0 -0
  94. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/utils/files/json_manager.py +0 -0
  95. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/utils/files/json_resources_loader.py +0 -0
  96. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/utils/files/metadata_handler.py +0 -0
  97. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/utils/files/run_bin.py +0 -0
  98. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/utils/files/sanitize_filename.py +0 -0
  99. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/utils/media/apple_png_normalize.py +0 -0
  100. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/utils/media/decrypt_kakao.py +0 -0
  101. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/utils/media/format_verify.py +0 -0
  102. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert/utils/url_detect.py +0 -0
  103. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert.egg-info/SOURCES.txt +0 -0
  104. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert.egg-info/dependency_links.txt +0 -0
  105. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert.egg-info/entry_points.txt +0 -0
  106. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert.egg-info/requires.txt +0 -0
  107. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/src/sticker_convert.egg-info/top_level.txt +0 -0
  108. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/tests/test_compression.py +0 -0
  109. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/tests/test_download.py +0 -0
  110. {sticker-convert-2.7.13 → sticker-convert-2.8.0}/tests/test_export.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sticker-convert
3
- Version: 2.7.13
3
+ Version: 2.8.0
4
4
  Summary: Convert (animated) stickers to/from WhatsApp, Telegram, Signal, Line, Kakao, iMessage. Written in Python.
5
5
  Author-email: laggykiller <chaudominic2@gmail.com>
6
6
  Maintainer-email: laggykiller <chaudominic2@gmail.com>
@@ -40,7 +40,7 @@ Repository = "https://github.com/laggykiller/sticker-convert"
40
40
  sticker-convert = "sticker_convert.__main__:main"
41
41
 
42
42
  [build-system]
43
- requires = ["setuptools>=61.0.0", "wheel"]
43
+ requires = ["setuptools>=61.0.0"]
44
44
  build-backend = "setuptools.build_meta"
45
45
 
46
46
  [tool.setuptools.packages.find]
@@ -1,6 +1,5 @@
1
1
  #!/usr/bin/env python3
2
2
  import os
3
- from decimal import ROUND_HALF_UP, Decimal
4
3
  from fractions import Fraction
5
4
  from io import BytesIO
6
5
  from math import ceil, floor
@@ -15,7 +14,7 @@ from PIL import __version__ as PillowVersion
15
14
  from sticker_convert.job_option import CompOption
16
15
  from sticker_convert.utils.callback import Callback, CallbackReturn, CbQueueItemType
17
16
  from sticker_convert.utils.files.cache_store import CacheStore
18
- from sticker_convert.utils.media.codec_info import CodecInfo
17
+ from sticker_convert.utils.media.codec_info import CodecInfo, rounding
19
18
  from sticker_convert.utils.media.format_verify import FormatVerify
20
19
 
21
20
  if TYPE_CHECKING:
@@ -44,10 +43,6 @@ YUV_RGB_MATRIX = np.array(
44
43
  )
45
44
 
46
45
 
47
- def rounding(value: float) -> Decimal:
48
- return Decimal(value).quantize(0, ROUND_HALF_UP)
49
-
50
-
51
46
  def get_step_value(
52
47
  max_step: Optional[int],
53
48
  min_step: Optional[int],
@@ -426,10 +421,24 @@ class StickerConvert:
426
421
  def _frames_import_pillow(self) -> None:
427
422
  with Image.open(self.in_f) as im:
428
423
  # Note: im.convert("RGBA") would return rgba image of current frame only
429
- if "n_frames" in dir(im):
430
- for i in range(im.n_frames):
431
- im.seek(i)
424
+ if (
425
+ "n_frames" in dir(im)
426
+ and im.n_frames != 0
427
+ and self.codec_info_orig.fps != 0.0
428
+ ):
429
+ duration_ptr = 0.0
430
+ duration_inc = 1 / self.codec_info_orig.fps * 1000
431
+ next_frame_start_duration = im.info.get("duration", 1000)
432
+ frame = 0
433
+ while True:
432
434
  self.frames_raw.append(np.asarray(im.convert("RGBA")))
435
+ duration_ptr += duration_inc
436
+ if duration_ptr >= next_frame_start_duration:
437
+ if frame == im.n_frames:
438
+ break
439
+ im.seek(frame)
440
+ next_frame_start_duration += im.info.get("duration", 1000)
441
+ frame += 1
433
442
  else:
434
443
  self.frames_raw.append(np.asarray(im.convert("RGBA")))
435
444
 
@@ -480,10 +489,12 @@ class StickerConvert:
480
489
  graph.configure()
481
490
 
482
491
  graph.push(frame)
483
- frame = cast(VideoFrame, graph.pull())
492
+ frame_resized = cast(VideoFrame, graph.pull())
493
+ else:
494
+ frame_resized = frame
484
495
 
485
- if frame.format.name == "yuv420p":
486
- rgb_array = frame.to_ndarray(format="rgb24")
496
+ if frame_resized.format.name == "yuv420p":
497
+ rgb_array = frame_resized.to_ndarray(format="rgb24")
487
498
  rgba_array = np.dstack(
488
499
  (
489
500
  rgb_array,
@@ -494,11 +505,11 @@ class StickerConvert:
494
505
  # yuva420p may cause crash
495
506
  # Not safe to directly call frame.to_ndarray(format="rgba")
496
507
  # https://github.com/laggykiller/sticker-convert/issues/114
497
- frame = frame.reformat(
508
+ frame_resized = frame_resized.reformat(
498
509
  format="yuva420p",
499
510
  dst_colorspace=1,
500
511
  )
501
- rgba_array = yuva_to_rgba(frame)
512
+ rgba_array = yuva_to_rgba(frame_resized)
502
513
 
503
514
  # Remove pixels that was added to make dimensions even
504
515
  rgba_array = rgba_array[0:width_orig, 0:height_orig]
@@ -534,25 +545,25 @@ class StickerConvert:
534
545
  anim.lottie_animation_destroy()
535
546
 
536
547
  def determine_bg_color(self) -> Tuple[int, int, int, int]:
548
+ mean_total = 0.0
537
549
  # Calculate average color of all frames for selecting background color
538
- frames_raw_np = np.array(self.frames_raw)
539
- s = frames_raw_np.shape
540
- colors = frames_raw_np.reshape((-1, s[3])) # type: ignore
541
- # Do not count in alpha=0
542
- # If alpha > 0, use alpha as weight
543
- colors = colors[colors[:, 3] != 0]
544
- if colors.shape[0] == 0:
545
- return (0, 0, 0, 0)
550
+ for frame in self.frames_raw:
551
+ s = frame.shape
552
+ colors = frame.reshape((-1, s[2])) # type: ignore
553
+ # Do not count in alpha=0
554
+ # If alpha > 0, use alpha as weight
555
+ colors = colors[colors[:, 3] != 0]
556
+ if colors.shape[0] != 0:
557
+ alphas = colors[:, 3] / 255
558
+ r_mean = np.mean(colors[:, 0] * alphas)
559
+ g_mean = np.mean(colors[:, 1] * alphas)
560
+ b_mean = np.mean(colors[:, 2] * alphas)
561
+ mean_total += (r_mean + g_mean + b_mean) / 3
562
+
563
+ if mean_total / len(self.frames_raw) < 128:
564
+ return (255, 255, 255, 0)
546
565
  else:
547
- alphas = colors[:, 3] / 255
548
- r_mean = np.mean(colors[:, 0] * alphas)
549
- g_mean = np.mean(colors[:, 1] * alphas)
550
- b_mean = np.mean(colors[:, 2] * alphas)
551
- mean = (r_mean + g_mean + b_mean) / 3
552
- if mean < 128:
553
- return (255, 255, 255, 0)
554
- else:
555
- return (0, 0, 0, 0)
566
+ return (0, 0, 0, 0)
556
567
 
557
568
  def frames_resize(
558
569
  self, frames_in: "List[np.ndarray[Any, Any]]"
@@ -647,8 +658,6 @@ class StickerConvert:
647
658
  frame_current = 0
648
659
  frame_current_float = 0.0
649
660
  while True:
650
- frame_current_float += frame_increment
651
- frame_current = int(rounding(frame_current_float))
652
661
  if frame_current <= len(frames_in) - 1 and not (
653
662
  frames_out_max and len(frames_out) == frames_out_max
654
663
  ):
@@ -659,6 +668,8 @@ class StickerConvert:
659
668
  ):
660
669
  frames_out.append(frames_in[-1])
661
670
  return frames_out
671
+ frame_current_float += frame_increment
672
+ frame_current = int(rounding(frame_current_float))
662
673
 
663
674
  def frames_export(self) -> None:
664
675
  is_animated = len(self.frames_processed) > 1 and self.fps
@@ -775,10 +786,25 @@ class StickerConvert:
775
786
  config = webp.WebPConfig.new(quality=self.quality) # type: ignore
776
787
  enc = webp.WebPAnimEncoder.new(self.res_w, self.res_h) # type: ignore
777
788
  timestamp_ms = 0
778
- for frame in self.frames_processed:
779
- pic = webp.WebPPicture.from_numpy(frame) # type: ignore
789
+ timestamp_inc = int(1000 / self.fps)
790
+
791
+ pic = webp.WebPPicture.from_numpy(self.frames_processed[0]) # type: ignore
792
+ enc.encode_frame(pic, 0, config=config) # type: ignore
793
+
794
+ frame_num = 1
795
+ frame_num_prev = 1
796
+ frame_total = len(self.frames_processed)
797
+ while frame_num < frame_total - 1:
798
+ while frame_num < frame_total - 1 and np.array_equal(
799
+ self.frames_processed[frame_num_prev],
800
+ self.frames_processed[frame_num],
801
+ ):
802
+ timestamp_ms += timestamp_inc
803
+ frame_num += 1
804
+ pic = webp.WebPPicture.from_numpy(self.frames_processed[frame_num]) # type: ignore
780
805
  enc.encode_frame(pic, timestamp_ms, config=config) # type: ignore
781
- timestamp_ms += int(1000 / self.fps)
806
+ frame_num_prev = frame_num
807
+
782
808
  anim_data = enc.assemble(timestamp_ms) # type: ignore
783
809
  self.tmp_f.write(anim_data.buffer()) # type: ignore
784
810
 
@@ -3,13 +3,64 @@ from __future__ import annotations
3
3
 
4
4
  import mmap
5
5
  from decimal import ROUND_HALF_UP, Decimal
6
+ from fractions import Fraction
6
7
  from io import BytesIO
8
+ from math import gcd
7
9
  from pathlib import Path
8
- from typing import BinaryIO, Optional, Tuple, Union, cast
10
+ from typing import BinaryIO, List, Optional, Tuple, Union, cast
9
11
 
10
12
  from PIL import Image, UnidentifiedImageError
11
13
 
12
14
 
15
+ def lcm(a: int, b: int):
16
+ return abs(a * b) // gcd(a, b)
17
+
18
+
19
+ def rounding(value: float) -> Decimal:
20
+ return Decimal(value).quantize(0, ROUND_HALF_UP)
21
+
22
+
23
+ def fraction_gcd(x: Fraction, y: Fraction) -> Fraction:
24
+ a = x.numerator
25
+ b = x.denominator
26
+ c = y.numerator
27
+ d = y.denominator
28
+ return Fraction(gcd(a, c), lcm(b, d))
29
+
30
+
31
+ def fractions_gcd(*fractions: Fraction) -> Fraction:
32
+ fractions_list = list(fractions)
33
+ gcd = fractions_list.pop(0)
34
+ for fraction in fractions_list:
35
+ gcd = fraction_gcd(gcd, fraction)
36
+
37
+ return gcd
38
+
39
+
40
+ def get_five_dec_place(value: float) -> str:
41
+ return str(value).split(".")[1][:5].ljust(5, "0")
42
+
43
+
44
+ def likely_int(value: float) -> bool:
45
+ if isinstance(value, int):
46
+ return True
47
+ return True if get_five_dec_place(value) in ("99999", "00000") else False
48
+
49
+
50
+ def durations_gcd(*durations: Union[int, float]) -> Union[float, Fraction]:
51
+ if any(i for i in durations if isinstance(i, float)):
52
+ if all(i for i in durations if likely_int(i)):
53
+ return Fraction(gcd(*(int(rounding(i)) for i in durations)), 1)
54
+ # Test for denominators that can produce recurring decimal
55
+ for x in (3, 6, 7, 9, 11, 13):
56
+ if all(likely_int(i * x) for i in durations):
57
+ return Fraction(gcd(*(int(rounding(i * x)) for i in durations)), x)
58
+ else:
59
+ return min(durations)
60
+ else:
61
+ return Fraction(gcd(*durations), 1) # type: ignore
62
+
63
+
13
64
  class CodecInfo:
14
65
  def __init__(
15
66
  self, file: Union[Path, bytes], file_ext: Optional[str] = None
@@ -29,8 +80,9 @@ class CodecInfo:
29
80
  @staticmethod
30
81
  def get_file_fps_frames_duration(
31
82
  file: Union[Path, bytes], file_ext: Optional[str] = None
32
- ) -> Tuple[float, int, int]:
83
+ ) -> Tuple[float, int, float]:
33
84
  fps: float
85
+ duration: float
34
86
 
35
87
  if not file_ext and isinstance(file, Path):
36
88
  file_ext = CodecInfo.get_file_ext(file)
@@ -41,13 +93,12 @@ class CodecInfo:
41
93
  duration = int(frames / fps * 1000)
42
94
  else:
43
95
  duration = 0
96
+ elif file_ext == ".webp":
97
+ fps, frames, duration = CodecInfo._get_file_fps_frames_duration_webp(file)
98
+ elif file_ext in (".gif", ".apng", ".png"):
99
+ fps, frames, duration = CodecInfo._get_file_fps_frames_duration_pillow(file)
44
100
  else:
45
- if file_ext == ".webp":
46
- frames, duration = CodecInfo._get_file_frames_duration_webp(file)
47
- elif file_ext in (".gif", ".apng", ".png"):
48
- frames, duration = CodecInfo._get_file_frames_duration_pillow(file)
49
- else:
50
- frames, duration = CodecInfo._get_file_frames_duration_av(file)
101
+ frames, duration = CodecInfo._get_file_frames_duration_av(file)
51
102
 
52
103
  if duration > 0:
53
104
  fps = frames / duration * 1000
@@ -63,14 +114,16 @@ class CodecInfo:
63
114
 
64
115
  if file_ext == ".tgs":
65
116
  return CodecInfo._get_file_fps_tgs(file)
66
- if file_ext == ".webp":
67
- frames, duration = CodecInfo._get_file_frames_duration_webp(file)
117
+ elif file_ext == ".webp":
118
+ fps, _, _ = CodecInfo._get_file_fps_frames_duration_webp(file)
119
+ return fps
68
120
  elif file_ext in (".gif", ".apng", ".png"):
69
- frames, duration = CodecInfo._get_file_frames_duration_pillow(file)
70
- else:
71
- frames, duration = CodecInfo._get_file_frames_duration_av(
72
- file, frames_to_iterate=10
73
- )
121
+ fps, _, _ = CodecInfo._get_file_fps_frames_duration_pillow(file)
122
+ return fps
123
+
124
+ frames, duration = CodecInfo._get_file_frames_duration_av(
125
+ file, frames_to_iterate=10
126
+ )
74
127
 
75
128
  if duration > 0:
76
129
  return frames / duration * 1000
@@ -89,7 +142,7 @@ class CodecInfo:
89
142
  if file_ext == ".tgs":
90
143
  return CodecInfo._get_file_frames_tgs(file)
91
144
  if file_ext in (".gif", ".webp", ".png", ".apng"):
92
- frames, _ = CodecInfo._get_file_frames_duration_pillow(
145
+ _, frames, _ = CodecInfo._get_file_fps_frames_duration_pillow(
93
146
  file, frames_only=True
94
147
  )
95
148
  else:
@@ -106,7 +159,9 @@ class CodecInfo:
106
159
  @staticmethod
107
160
  def get_file_duration(
108
161
  file: Union[Path, bytes], file_ext: Optional[str] = None
109
- ) -> int:
162
+ ) -> float:
163
+ duration: float
164
+
110
165
  # Return duration in miliseconds
111
166
  if not file_ext and isinstance(file, Path):
112
167
  file_ext = CodecInfo.get_file_ext(file)
@@ -118,9 +173,9 @@ class CodecInfo:
118
173
  else:
119
174
  duration = 0
120
175
  elif file_ext == ".webp":
121
- _, duration = CodecInfo._get_file_frames_duration_webp(file)
176
+ _, _, duration = CodecInfo._get_file_fps_frames_duration_webp(file)
122
177
  elif file_ext in (".gif", ".png", ".apng"):
123
- _, duration = CodecInfo._get_file_frames_duration_pillow(file)
178
+ _, _, duration = CodecInfo._get_file_fps_frames_duration_pillow(file)
124
179
  else:
125
180
  _, duration = CodecInfo._get_file_frames_duration_av(file)
126
181
 
@@ -176,29 +231,46 @@ class CodecInfo:
176
231
  return fps, frames
177
232
 
178
233
  @staticmethod
179
- def _get_file_frames_duration_pillow(
234
+ def _get_file_fps_frames_duration_pillow(
180
235
  file: Union[Path, bytes], frames_only: bool = False
181
- ) -> Tuple[int, int]:
182
- total_duration = 0
236
+ ) -> Tuple[float, int, float]:
237
+ total_duration = 0.0
238
+ durations: List[float] = []
183
239
 
184
240
  with Image.open(file) as im:
185
241
  if "n_frames" in dir(im):
186
242
  frames = im.n_frames
187
243
  if frames_only is True:
188
- return frames, 1
244
+ return 0.0, frames, 1
189
245
  for i in range(im.n_frames):
190
246
  im.seek(i)
191
- total_duration += im.info.get("duration", 1000)
192
- return frames, total_duration
193
-
194
- return 1, 0
247
+ frame_duration = cast(float, im.info.get("duration", 1000))
248
+ if frame_duration not in durations and frame_duration != 0:
249
+ durations.append(frame_duration)
250
+ total_duration += frame_duration
251
+ if im.n_frames == 0 or total_duration == 0:
252
+ fps = 0.0
253
+ elif len(durations) == 1:
254
+ fps = frames / total_duration * 1000
255
+ else:
256
+ duration_gcd = durations_gcd(*durations)
257
+ frames_apparent = total_duration / duration_gcd
258
+ fps = frames_apparent / total_duration * 1000
259
+ return fps, frames, total_duration
260
+
261
+ return (
262
+ 0.0,
263
+ 1,
264
+ 0,
265
+ )
195
266
 
196
267
  @staticmethod
197
- def _get_file_frames_duration_webp(
268
+ def _get_file_fps_frames_duration_webp(
198
269
  file: Union[Path, bytes],
199
- ) -> Tuple[int, int]:
270
+ ) -> Tuple[float, int, int]:
200
271
  total_duration = 0
201
272
  frames = 0
273
+ durations: List[int] = []
202
274
 
203
275
  def _open_f(file: Union[Path, bytes]) -> BinaryIO:
204
276
  if isinstance(file, Path):
@@ -213,16 +285,26 @@ class CodecInfo:
213
285
  break
214
286
  mm.seek(anmf_pos + 20)
215
287
  frame_duration_32 = mm.read(4)
216
- frame_duration = frame_duration_32[:-1] + bytes(
288
+ frame_duration_bytes = frame_duration_32[:-1] + bytes(
217
289
  int(frame_duration_32[-1]) & 0b11111100
218
290
  )
219
- total_duration += int.from_bytes(frame_duration, "little")
291
+ frame_duration = int.from_bytes(frame_duration_bytes, "little")
292
+ if frame_duration not in durations and frame_duration != 0:
293
+ durations.append(frame_duration)
294
+ total_duration += frame_duration
220
295
  frames += 1
221
296
 
222
297
  if frames == 0:
223
- return 1, 0
298
+ return 0.0, 0, 0
299
+
300
+ if len(durations) == 1:
301
+ fps = frames / total_duration * 1000
302
+ else:
303
+ duration_gcd = durations_gcd(*durations)
304
+ frames_apparent = total_duration / duration_gcd
305
+ fps = float(frames_apparent / total_duration * 1000)
224
306
 
225
- return frames, total_duration
307
+ return fps, frames, total_duration
226
308
 
227
309
  @staticmethod
228
310
  def _get_file_frames_duration_av(
@@ -246,9 +328,7 @@ class CodecInfo:
246
328
  container = cast(InputContainer, container)
247
329
  stream = container.streams.video[0]
248
330
  if container.duration:
249
- duration_metadata = int(
250
- Decimal(container.duration / 1000).quantize(0, ROUND_HALF_UP)
251
- )
331
+ duration_metadata = int(rounding(container.duration / 1000))
252
332
  else:
253
333
  duration_metadata = 0
254
334
 
@@ -273,7 +353,7 @@ class CodecInfo:
273
353
  duration_n_minus_one = last_frame.pts * time_base_ms
274
354
  ms_per_frame = duration_n_minus_one / (frame_count - 1)
275
355
  duration = frame_count * ms_per_frame
276
- return frame_count, int(Decimal(duration).quantize(0, ROUND_HALF_UP))
356
+ return frame_count, int(rounding(duration))
277
357
 
278
358
  return 0, 0
279
359
 
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env python3
2
2
 
3
- __version__ = "2.7.13"
3
+ __version__ = "2.8.0"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sticker-convert
3
- Version: 2.7.13
3
+ Version: 2.8.0
4
4
  Summary: Convert (animated) stickers to/from WhatsApp, Telegram, Signal, Line, Kakao, iMessage. Written in Python.
5
5
  Author-email: laggykiller <chaudominic2@gmail.com>
6
6
  Maintainer-email: laggykiller <chaudominic2@gmail.com>