lyrics-transcriber 0.30.1__py3-none-any.whl → 0.32.2__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 (84) hide show
  1. lyrics_transcriber/__init__.py +2 -1
  2. lyrics_transcriber/cli/cli_main.py +33 -12
  3. lyrics_transcriber/core/config.py +35 -0
  4. lyrics_transcriber/core/controller.py +85 -121
  5. lyrics_transcriber/correction/anchor_sequence.py +471 -0
  6. lyrics_transcriber/correction/corrector.py +237 -33
  7. lyrics_transcriber/correction/handlers/__init__.py +0 -0
  8. lyrics_transcriber/correction/handlers/base.py +30 -0
  9. lyrics_transcriber/correction/handlers/extend_anchor.py +91 -0
  10. lyrics_transcriber/correction/handlers/levenshtein.py +147 -0
  11. lyrics_transcriber/correction/handlers/no_space_punct_match.py +98 -0
  12. lyrics_transcriber/correction/handlers/relaxed_word_count_match.py +55 -0
  13. lyrics_transcriber/correction/handlers/repeat.py +71 -0
  14. lyrics_transcriber/correction/handlers/sound_alike.py +223 -0
  15. lyrics_transcriber/correction/handlers/syllables_match.py +182 -0
  16. lyrics_transcriber/correction/handlers/word_count_match.py +54 -0
  17. lyrics_transcriber/correction/handlers/word_operations.py +135 -0
  18. lyrics_transcriber/correction/phrase_analyzer.py +426 -0
  19. lyrics_transcriber/correction/text_utils.py +30 -0
  20. lyrics_transcriber/lyrics/base_lyrics_provider.py +5 -81
  21. lyrics_transcriber/lyrics/genius.py +5 -2
  22. lyrics_transcriber/lyrics/spotify.py +3 -3
  23. lyrics_transcriber/output/ass/__init__.py +21 -0
  24. lyrics_transcriber/output/{ass.py → ass/ass.py} +150 -690
  25. lyrics_transcriber/output/ass/ass_specs.txt +732 -0
  26. lyrics_transcriber/output/ass/config.py +37 -0
  27. lyrics_transcriber/output/ass/constants.py +23 -0
  28. lyrics_transcriber/output/ass/event.py +94 -0
  29. lyrics_transcriber/output/ass/formatters.py +132 -0
  30. lyrics_transcriber/output/ass/lyrics_line.py +219 -0
  31. lyrics_transcriber/output/ass/lyrics_screen.py +252 -0
  32. lyrics_transcriber/output/ass/section_detector.py +89 -0
  33. lyrics_transcriber/output/ass/section_screen.py +106 -0
  34. lyrics_transcriber/output/ass/style.py +187 -0
  35. lyrics_transcriber/output/cdg.py +503 -0
  36. lyrics_transcriber/output/cdgmaker/__init__.py +0 -0
  37. lyrics_transcriber/output/cdgmaker/cdg.py +262 -0
  38. lyrics_transcriber/output/cdgmaker/composer.py +1919 -0
  39. lyrics_transcriber/output/cdgmaker/config.py +151 -0
  40. lyrics_transcriber/output/cdgmaker/images/instrumental.png +0 -0
  41. lyrics_transcriber/output/cdgmaker/images/intro.png +0 -0
  42. lyrics_transcriber/output/cdgmaker/pack.py +507 -0
  43. lyrics_transcriber/output/cdgmaker/render.py +346 -0
  44. lyrics_transcriber/output/cdgmaker/transitions/centertexttoplogobottomtext.png +0 -0
  45. lyrics_transcriber/output/cdgmaker/transitions/circlein.png +0 -0
  46. lyrics_transcriber/output/cdgmaker/transitions/circleout.png +0 -0
  47. lyrics_transcriber/output/cdgmaker/transitions/fizzle.png +0 -0
  48. lyrics_transcriber/output/cdgmaker/transitions/largecentertexttoplogo.png +0 -0
  49. lyrics_transcriber/output/cdgmaker/transitions/rectangle.png +0 -0
  50. lyrics_transcriber/output/cdgmaker/transitions/spiral.png +0 -0
  51. lyrics_transcriber/output/cdgmaker/transitions/topleftmusicalnotes.png +0 -0
  52. lyrics_transcriber/output/cdgmaker/transitions/wipein.png +0 -0
  53. lyrics_transcriber/output/cdgmaker/transitions/wipeleft.png +0 -0
  54. lyrics_transcriber/output/cdgmaker/transitions/wipeout.png +0 -0
  55. lyrics_transcriber/output/cdgmaker/transitions/wiperight.png +0 -0
  56. lyrics_transcriber/output/cdgmaker/utils.py +132 -0
  57. lyrics_transcriber/output/fonts/AvenirNext-Bold.ttf +0 -0
  58. lyrics_transcriber/output/fonts/DMSans-VariableFont_opsz,wght.ttf +0 -0
  59. lyrics_transcriber/output/fonts/DMSerifDisplay-Regular.ttf +0 -0
  60. lyrics_transcriber/output/fonts/Oswald-SemiBold.ttf +0 -0
  61. lyrics_transcriber/output/fonts/Zurich_Cn_BT_Bold.ttf +0 -0
  62. lyrics_transcriber/output/fonts/arial.ttf +0 -0
  63. lyrics_transcriber/output/fonts/georgia.ttf +0 -0
  64. lyrics_transcriber/output/fonts/verdana.ttf +0 -0
  65. lyrics_transcriber/output/generator.py +101 -193
  66. lyrics_transcriber/output/lyrics_file.py +102 -0
  67. lyrics_transcriber/output/plain_text.py +91 -0
  68. lyrics_transcriber/output/segment_resizer.py +416 -0
  69. lyrics_transcriber/output/subtitles.py +328 -302
  70. lyrics_transcriber/output/video.py +219 -0
  71. lyrics_transcriber/review/__init__.py +1 -0
  72. lyrics_transcriber/review/server.py +138 -0
  73. lyrics_transcriber/transcribers/audioshake.py +3 -2
  74. lyrics_transcriber/transcribers/base_transcriber.py +5 -42
  75. lyrics_transcriber/transcribers/whisper.py +3 -4
  76. lyrics_transcriber/types.py +454 -0
  77. {lyrics_transcriber-0.30.1.dist-info → lyrics_transcriber-0.32.2.dist-info}/METADATA +14 -3
  78. lyrics_transcriber-0.32.2.dist-info/RECORD +86 -0
  79. {lyrics_transcriber-0.30.1.dist-info → lyrics_transcriber-0.32.2.dist-info}/WHEEL +1 -1
  80. {lyrics_transcriber-0.30.1.dist-info → lyrics_transcriber-0.32.2.dist-info}/entry_points.txt +1 -0
  81. lyrics_transcriber/correction/base_strategy.py +0 -29
  82. lyrics_transcriber/correction/strategy_diff.py +0 -263
  83. lyrics_transcriber-0.30.1.dist-info/RECORD +0 -25
  84. {lyrics_transcriber-0.30.1.dist-info → lyrics_transcriber-0.32.2.dist-info}/LICENSE +0 -0
@@ -0,0 +1,151 @@
1
+ from enum import StrEnum
2
+ from pathlib import Path
3
+ from typing import Any, TypeAlias
4
+
5
+ from attrs import define, field
6
+ from PIL import ImageColor
7
+
8
+
9
+ # NOTE RGBColor is specifically an RGB 3-tuple (red, green, blue).
10
+ RGBColor: TypeAlias = tuple[int, int, int]
11
+
12
+
13
+ def to_rgbcolor(val: Any) -> RGBColor:
14
+ if isinstance(val, str):
15
+ return ImageColor.getrgb(val)[:3]
16
+ raise TypeError("color value not convertible to RGBColor")
17
+
18
+
19
+ def to_rgbcolor_or_none(val: Any) -> RGBColor | None:
20
+ if val is None or val == "":
21
+ return None
22
+ if isinstance(val, str):
23
+ return ImageColor.getrgb(val)[:3]
24
+ raise TypeError("color value not convertible to RGBColor or None")
25
+
26
+
27
+ class LyricClearMode(StrEnum):
28
+ PAGE = "page"
29
+ LINE_EAGER = "eager"
30
+ LINE_DELAYED = "delayed"
31
+
32
+
33
+ class TextAlign(StrEnum):
34
+ LEFT = "left"
35
+ CENTER = "center"
36
+ RIGHT = "right"
37
+
38
+
39
+ class TextPlacement(StrEnum):
40
+ TOP_LEFT = "top left"
41
+ TOP_MIDDLE = "top middle"
42
+ TOP_RIGHT = "top right"
43
+ MIDDLE_LEFT = "middle left"
44
+ MIDDLE = "middle"
45
+ MIDDLE_RIGHT = "middle right"
46
+ BOTTOM_LEFT = "bottom left"
47
+ BOTTOM_MIDDLE = "bottom middle"
48
+ BOTTOM_RIGHT = "bottom right"
49
+
50
+
51
+ class StrokeType(StrEnum):
52
+ CIRCLE = "circle"
53
+ SQUARE = "square"
54
+ OCTAGON = "octagon"
55
+
56
+
57
+ @define
58
+ class SettingsInstrumental:
59
+ sync: int
60
+ line_tile_height: int
61
+
62
+ wait: bool = True
63
+ text: str = "INSTRUMENTAL"
64
+ text_align: TextAlign = TextAlign.CENTER
65
+ text_placement: TextPlacement = TextPlacement.MIDDLE
66
+ fill: RGBColor = field(converter=to_rgbcolor, default="#bbb")
67
+ stroke: RGBColor | None = field(
68
+ converter=to_rgbcolor_or_none,
69
+ default=None,
70
+ )
71
+ background: RGBColor | None = field(
72
+ converter=to_rgbcolor_or_none,
73
+ default=None,
74
+ )
75
+ image: Path | None = None
76
+ transition: str | None = None
77
+ x: int = 0
78
+ y: int = 0
79
+
80
+
81
+ @define
82
+ class SettingsSinger:
83
+ active_fill: RGBColor = field(converter=to_rgbcolor, default="#000")
84
+ active_stroke: RGBColor = field(converter=to_rgbcolor, default="#000")
85
+ inactive_fill: RGBColor = field(converter=to_rgbcolor, default="#000")
86
+ inactive_stroke: RGBColor = field(converter=to_rgbcolor, default="#000")
87
+
88
+
89
+ @define
90
+ class SettingsLyric:
91
+ sync: list[int]
92
+ text: str
93
+ line_tile_height: int
94
+ lines_per_page: int
95
+
96
+ singer: int = 1
97
+ row: int = 1
98
+
99
+
100
+ @define
101
+ class Settings:
102
+ title: str
103
+ artist: str
104
+ file: Path
105
+ font: Path
106
+ title_screen_background: Path
107
+ outro_background: Path
108
+
109
+ outname: str = "output"
110
+ clear_mode: LyricClearMode = LyricClearMode.LINE_DELAYED
111
+ sync_offset: int = 0
112
+ highlight_bandwidth: int = 1
113
+ draw_bandwidth: int = 1
114
+ background: RGBColor = field(converter=to_rgbcolor, default="black")
115
+ border: RGBColor | None = field(
116
+ converter=to_rgbcolor_or_none,
117
+ default="black",
118
+ )
119
+ font_size: int = 18
120
+ stroke_width: int = 0
121
+ stroke_type: StrokeType = StrokeType.OCTAGON
122
+ instrumentals: list[SettingsInstrumental] = field(factory=list)
123
+ singers: list[SettingsSinger] = field(factory=list)
124
+ lyrics: list[SettingsLyric] = field(factory=list)
125
+ title_color: RGBColor = field(converter=to_rgbcolor, default="#ffffff")
126
+ artist_color: RGBColor = field(converter=to_rgbcolor, default="#ffffff")
127
+ title_screen_transition: str = "centertexttoplogobottomtext"
128
+ title_artist_gap: int = 30
129
+
130
+ intro_duration_seconds: float = 5.0
131
+ first_syllable_buffer_seconds: float = 3.0
132
+
133
+ outro_transition: str = "centertexttoplogobottomtext"
134
+ outro_text_line1: str = "THANK YOU FOR SINGING!"
135
+ outro_text_line2: str = "nomadkaraoke.com"
136
+ outro_line1_line2_gap: int = 30
137
+ outro_line1_color: RGBColor = field(converter=to_rgbcolor, default="#ffffff")
138
+ outro_line2_color: RGBColor = field(converter=to_rgbcolor, default="#ffffff")
139
+
140
+
141
+ __all__ = [
142
+ "RGBColor",
143
+ "LyricClearMode",
144
+ "TextAlign",
145
+ "TextPlacement",
146
+ "StrokeType",
147
+ "SettingsInstrumental",
148
+ "SettingsSinger",
149
+ "SettingsLyric",
150
+ "Settings",
151
+ ]
@@ -0,0 +1,507 @@
1
+ from collections.abc import Collection
2
+ import itertools as it
3
+ import operator
4
+
5
+ from PIL import Image, ImageChops
6
+
7
+ from .cdg import *
8
+ from .render import *
9
+ from .utils import *
10
+
11
+
12
+ FILL = 0
13
+ STROKE = 1
14
+ HIGHLIGHT = 2
15
+
16
+
17
+ def image_section_to_tile_data(
18
+ image: Image.Image,
19
+ colors: Collection[int],
20
+ xy: tuple[int, int] = (0, 0),
21
+ ) -> list[int]:
22
+ """
23
+ Convert a section of an image to a list of CDG tile data bytes.
24
+
25
+ The 6x12 section of the image with `xy` at the top left corner is
26
+ converted to a list of CDG tile data bytes. If a pixel's color index
27
+ is in `colors`, it is converted to color 1; otherwise, it is
28
+ converted to color 0. Pixels outside of the image are considered to
29
+ have a color index of 0.
30
+
31
+ Parameters
32
+ ----------
33
+ image : `PIL.Image.Image`
34
+ Image to convert to tile data bytes.
35
+ colors : collection of int
36
+ Color indices to convert as color 1.
37
+ xy : tuple of (int, int), default (0, 0)
38
+ Top left corner of image section to convert.
39
+
40
+ Returns
41
+ -------
42
+ list of int
43
+ Tile data bytes.
44
+ """
45
+ x, y = xy
46
+ tile_data: list[int] = []
47
+ for image_y in range(y, y + CDG_TILE_HEIGHT):
48
+ # If image Y is out of bounds
49
+ if not (0 <= image_y < image.height):
50
+ # Every pixel in this row is considered 0
51
+ tile_data.append(CDG_MASK if 0 in colors else 0)
52
+ continue
53
+
54
+ tile_line = 0
55
+ for image_x in range(x, x + CDG_TILE_WIDTH):
56
+ if 0 <= image_x < image.width:
57
+ pixel = image.getpixel((image_x, image_y))
58
+ else:
59
+ # If image X is out of bounds, pixel is 0
60
+ pixel = 0
61
+ # Shift in one bit, which is "on" if the pixel is in the
62
+ # collection of colors and "off" otherwise
63
+ tile_line = (tile_line << 1) | (pixel in colors)
64
+ tile_data.append(tile_line)
65
+
66
+ return tile_data
67
+
68
+
69
+ def line_image_to_packets(
70
+ image: Image.Image,
71
+ xy: tuple[int, int],
72
+ fill: int = FILL,
73
+ stroke: int = STROKE,
74
+ background: int = 0,
75
+ erase: bool = False,
76
+ ) -> list[CDGPacket]:
77
+ """
78
+ Convert a karaoke line image to CDG packets.
79
+
80
+ Parameters
81
+ ----------
82
+ image : `PIL.Image.Image`
83
+ Image to convert.
84
+ xy : tuple of (int, int)
85
+ Position of top left corner of image on-screen.
86
+ fill : int, default 0
87
+ Color index of text fill.
88
+ stroke : int, default 1
89
+ Color index of text stroke.
90
+ background : int, default 0
91
+ Color index of background.
92
+ erase : bool, default False
93
+ If true, render the CDG packets to erase this karaoke line; if
94
+ false, render the CDG packets to draw this karaoke line.
95
+
96
+ Returns
97
+ -------
98
+ list of `CDGPacket`
99
+ CDG packets to draw this karaoke line image.
100
+ """
101
+ x, y = xy
102
+
103
+ width = ceildiv(
104
+ # Width includes the blank space to the left of the first tile
105
+ (x % CDG_TILE_WIDTH) + image.width,
106
+ CDG_TILE_WIDTH,
107
+ )
108
+ height = ceildiv(
109
+ # Height includes the blank space above the first tile
110
+ (y % CDG_TILE_HEIGHT) + image.height,
111
+ CDG_TILE_HEIGHT,
112
+ )
113
+
114
+ packets: list[CDGPacket] = []
115
+ # NOTE We iterate top-to-bottom, then left-to-right, so the effect
116
+ # is sweeping across the columns from left to right.
117
+ for tile_x, tile_y in it.product(range(width), range(height)):
118
+ image_x = tile_x * CDG_TILE_WIDTH - (x % CDG_TILE_WIDTH)
119
+ image_y = tile_y * CDG_TILE_HEIGHT - (y % CDG_TILE_HEIGHT)
120
+
121
+ row = y // CDG_TILE_HEIGHT + tile_y
122
+ column = x // CDG_TILE_WIDTH + tile_x
123
+ # Skip if row or column is out of bounds
124
+ if not (
125
+ 0 <= row < CDG_SCREEN_HEIGHT // CDG_TILE_HEIGHT
126
+ and 0 <= column < CDG_SCREEN_WIDTH // CDG_TILE_WIDTH
127
+ ):
128
+ continue
129
+
130
+ if erase:
131
+ # Draw blank tiles over non-blank parts of the image
132
+ tile_data = image_section_to_tile_data(
133
+ image,
134
+ colors=[RENDERED_BLANK],
135
+ xy=(image_x, image_y),
136
+ )
137
+ if any(t != CDG_MASK for t in tile_data):
138
+ packets.append(tile_block(
139
+ color0=background, color1=background,
140
+ row=row, column=column,
141
+ tile=[0 for _ in range(CDG_TILE_HEIGHT)],
142
+ ))
143
+ else:
144
+ # Draw stroke
145
+ tile_data = image_section_to_tile_data(
146
+ image,
147
+ colors=[RENDERED_STROKE],
148
+ xy=(image_x, image_y),
149
+ )
150
+ drew_stroke = False
151
+ if any(tile_data):
152
+ drew_stroke = True
153
+ packets.append(tile_block(
154
+ color0=background, color1=stroke,
155
+ row=row, column=column,
156
+ tile=tile_data,
157
+ ))
158
+
159
+ # Draw text
160
+ tile_data = image_section_to_tile_data(
161
+ image,
162
+ colors=[RENDERED_FILL],
163
+ xy=(image_x, image_y),
164
+ )
165
+ if any(tile_data):
166
+ packet_func = tile_block
167
+ color0 = background
168
+ color1 = fill
169
+ if drew_stroke:
170
+ packet_func = tile_block_xor
171
+ color0 = 0
172
+ color1 = fill ^ background
173
+ packets.append(packet_func(
174
+ color0=color0, color1=color1,
175
+ row=row, column=column,
176
+ tile=tile_data,
177
+ ))
178
+
179
+ return packets
180
+
181
+
182
+ def line_mask_to_packets(
183
+ image: Image.Image,
184
+ xy: tuple[int, int],
185
+ edges: tuple[int, int],
186
+ highlight: int = HIGHLIGHT,
187
+ ) -> list[CDGPacket]:
188
+ """
189
+ Convert a section of a karaoke mask image to CDG packets.
190
+
191
+ The section is assumed to take up only one column of tiles.
192
+
193
+ Parameters
194
+ ----------
195
+ image : `PIL.Image.Image`
196
+ Image to convert.
197
+ xy : tuple of (int, int)
198
+ Position of top left corner of image on-screen.
199
+ edges : tuple of (int, int)
200
+ X positions of left and right edges of section.
201
+ highlight : int, default 2
202
+ Number with necessary bits on for highlight. This is XORed with
203
+ the color index for pixels with highlight.
204
+
205
+ Returns
206
+ -------
207
+ list of `CDGPacket`
208
+ CDG packets to draw this section of this karaoke line mask.
209
+ """
210
+ x, y = xy
211
+ left_edge, right_edge = edges
212
+
213
+ # Create section mask
214
+ section_mask = Image.new("P", image.size, 0)
215
+ # Draw rectangle with color 255 with edges as boundaries
216
+ # NOTE The color index must be 255 for ImageChops.multiply to
217
+ # preserve the color indices.
218
+ section_mask.paste(255, (left_edge - x, 0, right_edge - x, image.height))
219
+ # Mask out section of image
220
+ section = ImageChops.multiply(image, section_mask)
221
+
222
+ height = ceildiv(
223
+ # Height includes the blank space above the first tile
224
+ (y % CDG_TILE_HEIGHT) + image.height,
225
+ CDG_TILE_HEIGHT,
226
+ )
227
+
228
+ packets: list[CDGPacket] = []
229
+ tile_x = (left_edge // CDG_TILE_WIDTH) - (x // CDG_TILE_WIDTH)
230
+ # For all tiles in this column
231
+ for tile_y in range(height):
232
+ image_x = tile_x * CDG_TILE_WIDTH - (x % CDG_TILE_WIDTH)
233
+ image_y = tile_y * CDG_TILE_HEIGHT - (y % CDG_TILE_HEIGHT)
234
+
235
+ row = y // CDG_TILE_HEIGHT + tile_y
236
+ column = x // CDG_TILE_WIDTH + tile_x
237
+ # Skip if row or column is out of bounds
238
+ if not (
239
+ 0 <= row < CDG_SCREEN_HEIGHT // CDG_TILE_HEIGHT
240
+ and 0 <= column < CDG_SCREEN_WIDTH // CDG_TILE_WIDTH
241
+ ):
242
+ continue
243
+
244
+ # Draw mask section as highlight
245
+ tile_data = image_section_to_tile_data(
246
+ section,
247
+ colors=[RENDERED_MASK],
248
+ xy=(image_x, image_y),
249
+ )
250
+ if any(tile_data):
251
+ packets.append(tile_block_xor(
252
+ color0=0, color1=highlight,
253
+ row=row, column=column,
254
+ tile=tile_data,
255
+ ))
256
+
257
+ return packets
258
+
259
+
260
+ def image_to_packets(
261
+ image: Image.Image,
262
+ xy: tuple[int, int] = (0, 0),
263
+ background: Image.Image | None = None,
264
+ ) -> dict[tuple[int, int], list[CDGPacket]]:
265
+ """
266
+ Convert an image to CDG packets.
267
+
268
+ Parameters
269
+ ----------
270
+ image : `PIL.Image.Image`
271
+ Image to convert.
272
+ xy : tuple of (int, int), default (0, 0)
273
+ Position to draw image on screen.
274
+
275
+ Returns
276
+ -------
277
+ dict of {tuple of (int, int): list of CDGPacket}
278
+ Tile positions, and CDG packets to draw at those positions.
279
+ """
280
+ # Image must be in palette mode
281
+ assert image.mode == "P"
282
+ # Image must have correct number of colors
283
+ # HACK Assuming the palette is in RGB, there should be one palette
284
+ # entry per band (R, G, B). I don't know of a better way to count
285
+ # palette entries.
286
+ # REVIEW Is there a better way to count palette entries?
287
+ palette_mode, palette_data = image.palette.getdata()
288
+ assert palette_mode == "RGB"
289
+ assert len(palette_data) // 3 <= 16
290
+
291
+ # Same things apply to the background image, if any
292
+ if background is not None:
293
+ assert background.mode == "P"
294
+ bg_palette_mode, bg_palette_data = background.palette.getdata()
295
+ assert bg_palette_mode == "RGB"
296
+ assert len(bg_palette_data) // 3 <= 16
297
+
298
+ x, y = xy
299
+
300
+ width = ceildiv(
301
+ # Width includes the blank space to the left of the first tile
302
+ (x % CDG_TILE_WIDTH) + image.width,
303
+ CDG_TILE_WIDTH,
304
+ )
305
+ height = ceildiv(
306
+ # Height includes the blank space above the first tile
307
+ (y % CDG_TILE_HEIGHT) + image.height,
308
+ CDG_TILE_HEIGHT,
309
+ )
310
+
311
+ packets: dict[tuple[int, int], list[CDGPacket]] = {}
312
+ for tile_y, tile_x in it.product(range(height), range(width)):
313
+ image_x = tile_x * CDG_TILE_WIDTH - (x % CDG_TILE_WIDTH)
314
+ image_y = tile_y * CDG_TILE_HEIGHT - (y % CDG_TILE_HEIGHT)
315
+
316
+ row = y // CDG_TILE_HEIGHT + tile_y
317
+ column = x // CDG_TILE_WIDTH + tile_x
318
+ # Skip if row or column is out of bounds
319
+ if not (
320
+ 0 <= row < CDG_SCREEN_HEIGHT // CDG_TILE_HEIGHT
321
+ and 0 <= column < CDG_SCREEN_WIDTH // CDG_TILE_WIDTH
322
+ ):
323
+ continue
324
+
325
+ tile = image.crop((
326
+ image_x, image_y,
327
+ image_x + CDG_TILE_WIDTH, image_y + CDG_TILE_HEIGHT,
328
+ ))
329
+ background_tile = None
330
+ if background is not None:
331
+ background_tile = background.crop((
332
+ image_x, image_y,
333
+ image_x + CDG_TILE_WIDTH, image_y + CDG_TILE_HEIGHT,
334
+ ))
335
+ packets[(row, column)] = tile_to_packets(
336
+ tile, row, column,
337
+ background_tile=background_tile,
338
+ )
339
+
340
+ return packets
341
+
342
+
343
+ # REVIEW How can I change this function to draw an image over already
344
+ # existing pixels on the screen? For example, sometimes it would be more
345
+ # efficient to XOR over existing pixels than to draw a new tile.
346
+ def tile_to_packets(
347
+ tile: Image.Image,
348
+ row: int,
349
+ column: int,
350
+ background_tile: Image.Image | None = None,
351
+ ) -> list[CDGPacket]:
352
+ """
353
+ Convert a tile to CDG packets.
354
+
355
+ The tile is assumed to be a 6x12 image in `P` mode.
356
+
357
+ Parameters
358
+ ----------
359
+ tile : `PIL.Image.Image`
360
+ Tile to convert.
361
+ row : int
362
+ Row to draw tile on screen.
363
+ column : int
364
+ Column to draw tile on screen.
365
+
366
+ Returns
367
+ -------
368
+ list of CDGPacket
369
+ CDG packets to draw this tile.
370
+ """
371
+ # If the background tile has the same pixels as the tile we want to
372
+ # draw, don't draw this tile
373
+ if (
374
+ background_tile is not None
375
+ and list(tile.getdata()) == list(background_tile.getdata())
376
+ ):
377
+ return []
378
+
379
+ # Sort colors in descending order by frequency
380
+ colors: list[int] = list(map(
381
+ operator.itemgetter(1),
382
+ sorted(tile.getcolors(), reverse=True),
383
+ ))
384
+
385
+ if len(colors) == 1:
386
+ # HACK If the only color is 0 (and we're not drawing over a
387
+ # background tile), we don't draw this tile. This is not always
388
+ # desirable, but it's fine for our purposes.
389
+ if background_tile is None and not colors[0]:
390
+ return []
391
+ return [
392
+ tile_block(
393
+ color0=0, color1=colors[0],
394
+ row=row, column=column,
395
+ tile=[CDG_MASK] * CDG_TILE_HEIGHT,
396
+ ),
397
+ ]
398
+
399
+ if len(colors) == 2:
400
+ return [
401
+ tile_block(
402
+ color0=colors[1], color1=colors[0],
403
+ row=row, column=column,
404
+ tile=image_section_to_tile_data(tile, [colors[0]]),
405
+ ),
406
+ ]
407
+
408
+ if len(colors) == 3:
409
+ return [
410
+ tile_block(
411
+ color0=colors[1], color1=colors[0],
412
+ row=row, column=column,
413
+ tile=image_section_to_tile_data(tile, [colors[0]]),
414
+ ),
415
+ tile_block_xor(
416
+ color0=0, color1=colors[1] ^ colors[2],
417
+ row=row, column=column,
418
+ tile=image_section_to_tile_data(tile, [colors[2]]),
419
+ ),
420
+ ]
421
+
422
+ colors_or = 0x00
423
+ colors_xor = 0x00
424
+ colors_and = 0xff
425
+ for color in colors:
426
+ colors_or |= color
427
+ colors_xor ^= color
428
+ colors_and &= color
429
+ and_bits = colors_and.bit_count()
430
+ or_bits = colors_or.bit_count()
431
+ used_bits = or_bits - and_bits
432
+
433
+ if len(colors) == 4 and used_bits > 2 and colors_xor != 0:
434
+ return [
435
+ tile_block(
436
+ color0=colors[0], color1=colors[1],
437
+ row=row, column=column,
438
+ tile=image_section_to_tile_data(
439
+ tile, [colors[1], colors[2], colors[3]],
440
+ ),
441
+ ),
442
+ tile_block_xor(
443
+ color0=0, color1=colors[1] ^ colors[2],
444
+ row=row, column=column,
445
+ tile=image_section_to_tile_data(
446
+ tile, [colors[2]],
447
+ ),
448
+ ),
449
+ tile_block_xor(
450
+ color0=0, color1=colors[1] ^ colors[3],
451
+ row=row, column=column,
452
+ tile=image_section_to_tile_data(
453
+ tile, [colors[3]],
454
+ ),
455
+ ),
456
+ ]
457
+
458
+ if len(colors) > 4 or colors_xor != 0:
459
+ tile_packets: list[CDGPacket] = []
460
+
461
+ packet_func = tile_block
462
+ for i in range(4):
463
+ if not colors_or & (1 << i):
464
+ continue
465
+ if colors_and & (1 << i):
466
+ continue
467
+
468
+ color0 = 0
469
+ color1 = 1 << i
470
+ if packet_func == tile_block and colors_and:
471
+ color0 |= colors_and
472
+ color1 |= colors_and
473
+
474
+ tile_packets.append(packet_func(
475
+ color0=color0, color1=color1,
476
+ row=row, column=column,
477
+ tile=image_section_to_tile_data(
478
+ tile,
479
+ [color for color in range(16) if color & (1 << i)],
480
+ ),
481
+ ))
482
+ packet_func = tile_block_xor
483
+ return tile_packets
484
+
485
+ assert colors[2] ^ colors[0] == colors[1] ^ colors[3]
486
+ return [
487
+ tile_block(
488
+ color0=colors[1], color1=colors[0],
489
+ row=row, column=column,
490
+ tile=image_section_to_tile_data(
491
+ tile, [colors[0], colors[2]],
492
+ ),
493
+ ),
494
+ tile_block_xor(
495
+ color0=0, color1=colors[2] ^ colors[0],
496
+ row=row, column=column,
497
+ tile=image_section_to_tile_data(
498
+ tile, [colors[2], colors[3]],
499
+ ),
500
+ ),
501
+ ]
502
+
503
+
504
+ __all__ = [
505
+ "image_section_to_tile_data", "line_image_to_packets",
506
+ "line_mask_to_packets", "image_to_packets", "tile_to_packets",
507
+ ]