slidegen.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  1. #!/usr/bin/env python3
  2. """
  3. Copyright © 2022 Noah Vogt <noah@noahvogt.com>
  4. This program is free software: you can redistribute it and/or modify
  5. it under the terms of the GNU General Public License as published by
  6. the Free Software Foundation, either version 3 of the License, or
  7. (at your option) any later version.
  8. This program is distributed in the hope that it will be useful,
  9. but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. GNU General Public License for more details.
  12. You should have received a copy of the GNU General Public License
  13. along with this program. If not, see <http://www.gnu.org/licenses/>.
  14. """
  15. from os import path
  16. import re
  17. import sys
  18. import colorama
  19. from wand.exceptions import BlobError
  20. from wand.image import Image
  21. from utils import (
  22. log,
  23. error_msg,
  24. get_songtext_by_structure,
  25. structure_as_list,
  26. get_unique_structure_elements,
  27. )
  28. from slides import ClassicSongTemplate, ClassicStartSlide, ClassicSongSlide
  29. import config as const
  30. class Slidegen:
  31. def __init__(
  32. self, song_template_form, start_slide_form, song_slide_form
  33. ) -> None:
  34. self.metadata: dict = {"": ""}
  35. self.songtext: dict = {"": ""}
  36. self.song_file_path: str = ""
  37. self.song_file_content: list = []
  38. self.output_dir: str = ""
  39. self.chosen_structure: list | str = ""
  40. self.generated_slides: list = []
  41. self.song_template_form = song_template_form
  42. self.start_slide_form = start_slide_form
  43. self.song_slide_form = song_slide_form
  44. self.parse_argv()
  45. def execute(self) -> None:
  46. self.parse_file()
  47. self.calculate_desired_structures()
  48. self.generate_slides()
  49. def count_number_of_slides_to_be_generated(self) -> int:
  50. slide_count: int = 0
  51. for structure in self.chosen_structure:
  52. line_count: int = len(self.songtext[structure].splitlines())
  53. if line_count > const.STRUCTURE_ELEMENT_MAX_LINES:
  54. slide_count += (
  55. line_count // const.STRUCTURE_ELEMENT_MAX_LINES + 1
  56. )
  57. else:
  58. slide_count += 1
  59. return slide_count
  60. def generate_slides(self) -> None:
  61. template_img: Image = self.generate_song_template()
  62. slide_count: int = self.count_number_of_slides_to_be_generated()
  63. zfill_length: int = len(str(slide_count))
  64. self.generate_start_slide(template_img, zfill_length)
  65. self.generate_song_slides(slide_count, template_img, zfill_length)
  66. def generate_song_template(self) -> Image:
  67. song_template = self.song_template_form()
  68. log("generating template...")
  69. return song_template.get_template(self.metadata["title"])
  70. def generate_song_slides(
  71. self, slide_count, template_img, zfill_length
  72. ) -> None:
  73. log("generating song slides...")
  74. # unique_structures: list = list(set(self.chosen_structure))
  75. current_slide_index: int = 0
  76. for index, structure in enumerate(self.chosen_structure):
  77. structure_element_splitted: list = self.songtext[
  78. structure
  79. ].splitlines()
  80. line_count = len(structure_element_splitted)
  81. use_line_ranges_per_index = []
  82. use_lines_per_index = []
  83. if line_count <= const.STRUCTURE_ELEMENT_MAX_LINES:
  84. inner_slide_count = 1
  85. else:
  86. inner_slide_count: int = (
  87. line_count // const.STRUCTURE_ELEMENT_MAX_LINES + 1
  88. )
  89. use_lines_per_index = [
  90. line_count // inner_slide_count
  91. ] * inner_slide_count
  92. for inner_slide in range(inner_slide_count):
  93. if sum(use_lines_per_index) == line_count:
  94. break
  95. use_lines_per_index[inner_slide] = (
  96. use_lines_per_index[inner_slide] + 1
  97. )
  98. for inner_slide in range(inner_slide_count):
  99. use_line_ranges_per_index.append(
  100. sum(use_lines_per_index[:inner_slide])
  101. )
  102. for inner_slide in range(inner_slide_count):
  103. current_slide_index += 1
  104. log(
  105. "generating song slide [{} / {}]...".format(
  106. current_slide_index, slide_count
  107. )
  108. )
  109. if inner_slide_count == 1:
  110. structure_element_value: str = self.songtext[structure]
  111. else:
  112. splitted_wanted_range: list = structure_element_splitted[
  113. use_line_ranges_per_index[
  114. inner_slide
  115. ] : use_line_ranges_per_index[inner_slide]
  116. + use_lines_per_index[inner_slide]
  117. ]
  118. structure_element_value: str = ""
  119. for element in splitted_wanted_range:
  120. structure_element_value += element + "\n"
  121. structure_element_value = structure_element_value[:-1]
  122. song_slide = self.song_slide_form()
  123. song_slide_img = song_slide.get_slide(
  124. template_img,
  125. structure_element_value,
  126. self.chosen_structure,
  127. index,
  128. bool(
  129. inner_slide_count != 1
  130. and inner_slide != inner_slide_count - 1
  131. ),
  132. )
  133. song_slide_img.format = const.IMAGE_FORMAT
  134. try:
  135. song_slide_img.save(
  136. filename=path.join(
  137. self.output_dir,
  138. const.FILE_NAMEING
  139. + str(current_slide_index + 1).zfill(zfill_length)
  140. + "."
  141. + const.FILE_EXTENSION,
  142. )
  143. )
  144. except BlobError:
  145. error_msg("could not write slide to target directory")
  146. def generate_start_slide(self, template_img, zfill_length) -> None:
  147. log("generating start slide...")
  148. first_slide = self.start_slide_form()
  149. start_slide_img = first_slide.get_slide(
  150. template_img,
  151. self.metadata["book"],
  152. self.metadata["text"],
  153. self.metadata["melody"],
  154. )
  155. start_slide_img.format = const.IMAGE_FORMAT
  156. try:
  157. start_slide_img.save(
  158. filename=path.join(
  159. self.output_dir,
  160. const.FILE_NAMEING
  161. + "1".zfill(zfill_length)
  162. + "."
  163. + const.FILE_EXTENSION,
  164. )
  165. )
  166. except BlobError:
  167. error_msg("could not write start slide to target directory")
  168. def parse_file(self) -> None:
  169. self.parse_metadata()
  170. self.parse_songtext()
  171. def parse_metadata(self) -> None:
  172. metadata_dict = dict.fromkeys(const.METADATA_STRINGS)
  173. try:
  174. with open(self.song_file_path, mode="r", encoding="utf8") as opener:
  175. content = opener.readlines()
  176. except IOError:
  177. error_msg(
  178. "could not read the the song input file: '{}'".format(
  179. self.song_file_path
  180. )
  181. )
  182. valid_metadata_strings = list(const.METADATA_STRINGS)
  183. for line_nr, line in enumerate(content):
  184. if len(valid_metadata_strings) == 0:
  185. content = content[line_nr:]
  186. break
  187. if not re.match(
  188. r"^(?!structure)\S+: .+|^structure: ([0-9]+|R)(,([0-9]+|R))*$",
  189. line,
  190. ):
  191. if line[-1] == "\n":
  192. line = line[:-1]
  193. missing_metadata_strs = ""
  194. for metadata_str in valid_metadata_strings:
  195. missing_metadata_strs += ", " + metadata_str
  196. missing_metadata_strs = missing_metadata_strs[2:]
  197. error_msg(
  198. "invalid metadata syntax on line {}:\n{}\nThe ".format(
  199. line_nr + 1, line
  200. )
  201. + "following metadata strings are still missing: {}".format(
  202. missing_metadata_strs
  203. )
  204. )
  205. metadata_str = line[: line.index(":")]
  206. if metadata_str in valid_metadata_strings:
  207. metadata_dict[metadata_str] = line[line.index(": ") + 2 : -1]
  208. valid_metadata_strings.remove(metadata_str)
  209. continue
  210. error_msg("invalid metadata string '{}'".format(metadata_str))
  211. self.metadata = metadata_dict
  212. self.song_file_content = content
  213. def parse_songtext(self) -> None:
  214. unique_structures = get_unique_structure_elements(
  215. structure_as_list(self.metadata["structure"])
  216. )
  217. output_dict = dict.fromkeys(unique_structures)
  218. for structure in unique_structures:
  219. output_dict[structure] = get_songtext_by_structure(
  220. self.song_file_content, structure
  221. )
  222. self.songtext = output_dict
  223. def calculate_desired_structures(self) -> None:
  224. full_structure_str = str(self.metadata["structure"])
  225. full_structure_list = structure_as_list(full_structure_str)
  226. if len(self.chosen_structure) == 0:
  227. self.chosen_structure = structure_as_list(full_structure_str)
  228. log("chosen structure: {}".format(str(self.chosen_structure)))
  229. return
  230. if not "-" in self.chosen_structure:
  231. self.chosen_structure = structure_as_list(
  232. str(self.chosen_structure)
  233. )
  234. log("chosen structure: {}".format(str(self.chosen_structure)))
  235. return
  236. dash_index = str(self.chosen_structure).find("-")
  237. start_verse = str(self.chosen_structure[:dash_index]).strip()
  238. end_verse = str(self.chosen_structure[dash_index + 1 :]).strip()
  239. try:
  240. if int(start_verse) >= int(end_verse):
  241. error_msg("{} < {} must be true".format(start_verse, end_verse))
  242. if start_verse not in full_structure_str:
  243. error_msg("structure {} unknown".format(start_verse))
  244. if end_verse not in full_structure_str:
  245. error_msg("structure {} unknown".format(end_verse))
  246. except (ValueError, IndexError):
  247. error_msg("please choose a valid integer for the song structure")
  248. start_index = full_structure_list.index(start_verse)
  249. if start_index != 0:
  250. if (
  251. full_structure_list[0] == "R"
  252. and full_structure_list[start_index - 1] == "R"
  253. ):
  254. start_index -= 1
  255. end_index = full_structure_list.index(end_verse)
  256. if end_index != len(full_structure_list) - 1:
  257. if (
  258. full_structure_list[-1] == "R"
  259. and full_structure_list[end_index + 1] == "R"
  260. ):
  261. end_index += 1
  262. self.chosen_structure = full_structure_list[start_index : end_index + 1]
  263. log("chosen structure: {}".format(str(self.chosen_structure)))
  264. def parse_argv(self) -> None:
  265. try:
  266. self.song_file_path = sys.argv[1]
  267. self.output_dir = sys.argv[2]
  268. except IndexError:
  269. error_msg("incorrect amount of arguments provided, exiting...")
  270. try:
  271. self.chosen_structure = sys.argv[3]
  272. if self.chosen_structure.strip() == "":
  273. self.chosen_structure = ""
  274. except IndexError:
  275. self.chosen_structure = ""
  276. log("parsing {}...".format(self.song_file_path))
  277. def main() -> None:
  278. colorama.init()
  279. slidegen: Slidegen = Slidegen(
  280. ClassicSongTemplate, ClassicStartSlide, ClassicSongSlide
  281. )
  282. slidegen.execute()
  283. if __name__ == "__main__":
  284. main()