xrnconv-cli 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465
  1. #!/usr/bin/env python3
  2. '''
  3. Software License
  4. Copyright (C) 2021-05-24 Xoronos
  5. This program is free software: you can redistribute it and/or modify
  6. it under the terms of the GNU General Public License as published by
  7. the Free Software Foundation, version 3.
  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 <https://www.gnu.org/licenses/>.
  14. '''
  15. '''
  16. Liabilities
  17. The software is provided "AS IS" without any warranty of any kind, either expressed,
  18. implied, or statutory, including, but not limited to, any warranty that the software
  19. will conform to specifications, any implied warranties of merchantability, fitness
  20. for a particular purpose, and freedom from infringement, and any warranty that the
  21. documentation will conform to the software, or any warranty that the software will
  22. be error free.
  23. In no event shall Xoronos be liable for any damages, including, but not limited to,
  24. direct, indirect, special or consequential damages, arising out of, resulting from,
  25. or in any way connected with this software, whether or not based upon warranty,
  26. contract, tort, or otherwise, whether or not injury was sustained by persons or
  27. property or otherwise, and whether or not loss was sustained from, or arose out of
  28. the results of, or use of, the software or services provided hereunder.
  29. To request the provided software under a different license you can contact us at
  30. support@xoronos.com
  31. '''
  32. from PIL import Image
  33. import numpy as np
  34. import pickle
  35. import soundfile as sf
  36. import mutagen.flac
  37. import zlib
  38. import struct
  39. import sys
  40. import json
  41. import os
  42. from splitjoin import msb_lsb_split
  43. from splitjoin import msb_lsb_join
  44. PNG = 0
  45. FLAC = 1
  46. def save_array_metadata_png(metadata_file, array, ftype):
  47. # Serialize the metadata (shape, data type, etc.)
  48. metadata = {
  49. 'shape': array.shape,
  50. 'size': array.size,
  51. 'dtype': array.dtype
  52. }
  53. # Serialize metadata to bytes
  54. metadata_bytes = pickle.dumps(metadata)
  55. crc32 = zlib.crc32( struct.pack('<c', bytes([ftype])) + metadata_bytes )
  56. with open(metadata_file, 'wb') as f:
  57. f.write(struct.pack('<c', bytes([ftype])))
  58. f.write(metadata_bytes)
  59. f.write(struct.pack('<I',crc32))
  60. def save_array_metadata_flac(metadata_file, flac_info, sample_rate, array, ftype, image_array ):
  61. # Serialize the metadata (shape, data type, etc.)
  62. metadata = {
  63. 'shape': array.shape,
  64. 'size': array.size,
  65. 'dtype': array.dtype,
  66. 'sample_rate': sample_rate,
  67. 'image_array': image_array,
  68. 'flac_info': flac_info
  69. }
  70. # Serialize metadata to bytes
  71. metadata_bytes = pickle.dumps(metadata)
  72. crc32 = zlib.crc32(struct.pack('<c', bytes([ftype])) + metadata_bytes)
  73. with open(metadata_file, 'wb') as f:
  74. f.write(struct.pack('<c', bytes([ftype])))
  75. f.write(metadata_bytes)
  76. f.write(struct.pack('<I', crc32))
  77. def load_array_metadata(metadata_file, extended_file_extension):
  78. # Deserialize the metadata
  79. with open(metadata_file, 'rb') as f:
  80. binary_data = f.read()
  81. # Extract stored ftype value
  82. stored_ftype = struct.unpack('<c', binary_data[0:1])[0]
  83. if ( int.from_bytes(stored_ftype,"little") != extended_file_extension ) :
  84. print('error in expected file type stored_ftype {} extended_file_extension {}'.format(stored_ftype , extended_file_extension))
  85. exit(-1)
  86. # Extract stored crc32 value
  87. stored_crc32 = struct.unpack('<I', binary_data[-4:])[0]
  88. # Extract stored metadata
  89. metadata_bytes = binary_data[1:-4]
  90. # Verify crc32
  91. calculated_crc32 = zlib.crc32(struct.pack('<c',stored_ftype) + metadata_bytes)
  92. if calculated_crc32 != stored_crc32 :
  93. print('CRC32 error: stored_crc32 {}, calculated_crc32 {}'.format(stored_crc32, calculated_crc32))
  94. exit(-1)
  95. else :
  96. return pickle.loads(metadata_bytes) , ord(stored_ftype)
  97. def save_array_data(msbs_file, lsbs_file, bitsize, bit_skip, array):
  98. msbs_array , lsbs_array = msb_lsb_split( array , bitsize, bit_skip )
  99. # Serialize the msbs array data
  100. with open(msbs_file, 'wb') as f:
  101. np.save(f, msbs_array)
  102. # Serialize the lsbs array data
  103. with open(lsbs_file, 'wb') as f:
  104. np.save(f, lsbs_array)
  105. def load_array_data( msbs_file, lsbs_file, bitsize, bit_skip, num_el ):
  106. # Deserialize the msbs data
  107. with open(msbs_file, 'rb') as f:
  108. msbs_array = np.load(f)
  109. # Deserialize the msbs data
  110. with open(lsbs_file, 'rb') as f:
  111. lsbs_array = np.load(f)
  112. array_data = msb_lsb_join( msbs_array.astype(np.uint8) , lsbs_array.astype(np.uint8) , num_el , bitsize , bit_skip )
  113. return array_data
  114. def reconstruct_array(metadata, data):
  115. # Reconstruct the numpy array using the deserialized metadata and data
  116. array = np.ndarray(metadata['shape'], dtype=metadata['dtype'], buffer=data)
  117. return array
  118. # Create an array from the four matrices
  119. def create_array_from_matrices(red_matrix, green_matrix, blue_matrix, alpha_matrix=None):
  120. if alpha_matrix is not None:
  121. img_array = np.dstack((red_matrix, green_matrix, blue_matrix, alpha_matrix))
  122. else:
  123. img_array = np.dstack((red_matrix, green_matrix, blue_matrix))
  124. return img_array
  125. # Split an array into the four matrices
  126. def split_array_into_matrices(img_array):
  127. red_matrix = img_array[:, :, 0]
  128. green_matrix = img_array[:, :, 1]
  129. blue_matrix = img_array[:, :, 2]
  130. if img_array.shape[2] == 4: # If array has alpha channel
  131. alpha_matrix = img_array[:, :, 3]
  132. return red_matrix, green_matrix, blue_matrix, alpha_matrix
  133. else:
  134. return red_matrix, green_matrix, blue_matrix, None
  135. def png_to_three_arrays(input_file, metadata_file, msbs_file, lsbs_file):
  136. # Open the PNG image
  137. img = Image.open(input_file)
  138. # Convert the image to a numpy array
  139. img_array = np.array(img)
  140. if img.mode == 'RGBA' :
  141. # Separate the RGBA
  142. red_channel = img_array[:,:,0]
  143. green_channel = img_array[:,:,1]
  144. blue_channel = img_array[:,:,2]
  145. alpha_channel = img_array[:,:,3]
  146. out_array = create_array_from_matrices ( red_channel, green_channel, blue_channel, alpha_channel )
  147. elif img.mode == 'RGB':
  148. # Separate the RGB
  149. red_channel = img_array[:,:,0]
  150. green_channel = img_array[:,:,1]
  151. blue_channel = img_array[:,:,2]
  152. out_array = create_array_from_matrices ( red_channel, green_channel, blue_channel, None )
  153. else:
  154. # If the image does not have colors
  155. out_array = np.array(img_array)
  156. ftype = PNG
  157. save_array_metadata_png(metadata_file, out_array, ftype )
  158. bitsize = out_array.dtype.itemsize * 8
  159. bit_skip = 0
  160. save_array_data(msbs_file, lsbs_file, bitsize, bit_skip, out_array.reshape(-1).astype(np.uint64))
  161. def two_arrays_to_png( metadata, data, output_file):
  162. bitsize = metadata['dtype'].itemsize * 8
  163. num_el = metadata['size']
  164. shape = metadata['shape']
  165. img_array = reconstruct_array(metadata, data)
  166. if len(shape) == 3 :
  167. red_matrix, green_matrix, blue_matrix, alpha_matrix = split_array_into_matrices(img_array)
  168. # Merge the matrices into a single array if an alpha matrix is provided
  169. if alpha_matrix is not None:
  170. img_array = np.dstack((red_matrix, green_matrix, blue_matrix, alpha_matrix))
  171. else:
  172. img_array = np.dstack((red_matrix, green_matrix, blue_matrix))
  173. else :
  174. img_array = np.array(img_array)
  175. # Create an image object from the array
  176. if bitsize == 8 :
  177. img = Image.fromarray(img_array.astype('uint8'))
  178. elif bitsize == 16 :
  179. img = Image.fromarray(img_array.astype('uint16'))
  180. # Save the image to a PNG file
  181. img.save(output_file)
  182. def flac_to_three_arrays(input_file, metadata_file, msbs_file, lsbs_file):
  183. out_array, sample_rate = sf.read(input_file)
  184. flac_info = mutagen.flac.FLAC(input_file)
  185. image_array = []
  186. for picture in flac_info.pictures :
  187. image_array.append(picture)
  188. ftype = FLAC
  189. bitsize = out_array.dtype.itemsize * 8
  190. save_array_metadata_flac(metadata_file, flac_info, sample_rate, out_array, ftype, image_array )
  191. bit_skip = 0
  192. if out_array.dtype == np.float64 :
  193. out_array = np.float64(out_array).view(np.uint64)
  194. bit_skip = 12
  195. elif out_array.dtype == np.float32 :
  196. out_array = np.float32(out_array).view(np.uint32)
  197. bit_skip = 9
  198. out_array = out_array.reshape(-1).astype(np.uint64)
  199. save_array_data(msbs_file, lsbs_file, bitsize, bit_skip, out_array)
  200. def two_arrays_to_flac(metadata, data, output_file):
  201. # Write audio data to FLAC file
  202. sf.write(output_file, data, metadata['sample_rate'])
  203. # Update FLAC metadata blocks
  204. flac_file = mutagen.flac.FLAC(output_file)
  205. flac_file.clear()
  206. # Copy metadata from the source FLAC file to the target FLAC file
  207. for key, value in metadata['flac_info'].items():
  208. flac_file[key] = value
  209. for picture in metadata['image_array'] :
  210. flac_file.add_picture(picture)
  211. flac_file.save()
  212. def three_arrays_to_file( metadata_file, msbs_file, lsbs_file, output_file, file_extension):
  213. metadata, ftype = load_array_metadata ( metadata_file, file_extension)
  214. bitsize = metadata['dtype'].itemsize * 8
  215. num_el = metadata['size']
  216. if ( metadata['dtype'].type == np.float64 ) :
  217. bit_skip = 12
  218. data = load_array_data ( msbs_file, lsbs_file, bitsize, bit_skip, num_el )
  219. data = np.uint64(data).view(np.float64)
  220. data = data.reshape(metadata['shape']).astype(metadata['dtype'].type)
  221. elif ( metadata['dtype'].type == np.float32 ) :
  222. bit_skip = 9
  223. data = load_array_data ( msbs_file, lsbs_file, bitsize, bit_skip, num_el )
  224. data = np.uint64(data).view(np.float64)
  225. else :
  226. bit_skip = 0
  227. data = load_array_data ( msbs_file, lsbs_file, bitsize, bit_skip, num_el )
  228. data = data.reshape(metadata['shape']).astype(metadata['dtype'].type)
  229. if ftype == PNG :
  230. two_arrays_to_png( metadata, data, output_file )
  231. elif ftype == FLAC :
  232. two_arrays_to_flac( metadata, data, output_file )
  233. ################
  234. # Parser lexer #
  235. ################
  236. tool_name_str = "xrnconv-cli"
  237. split_str = "--std2xrn"
  238. join_str = "--xrn2std"
  239. source_str = "-source"
  240. destination_str = "-destination"
  241. msbs_str = "-msbs"
  242. lsbs_str = "-lsbs"
  243. metadata_str = "-metadata"
  244. list_of_commands = [ split_str, join_str ]
  245. list_of_options = [ source_str, destination_str , msbs_str, lsbs_str, metadata_str ]
  246. list_of_everything = list_of_commands + list_of_options
  247. arglist = [0 for i in range(len(list_of_commands) + len(list_of_options))]
  248. def print_help ():
  249. print("help page for the xrnconv-cli command\n")
  250. print("to split a standard file in three files")
  251. print("{} {} {} srcfile {} msbfile {} lsbfile {} matadatafile ".format( tool_name_str, split_str, source_str, msbs_str, lsbs_str, metadata_str))
  252. print("\nto join three files in a standard file")
  253. print("{} {} {} dstfile {} msbfile {} lsbfile {} matadatafile ".format( tool_name_str, join_str, destination_str, msbs_str, lsbs_str, metadata_str))
  254. if ( len(sys.argv) == 2 ) and ( sys.argv[1] == "--help" ) :
  255. print_help ()
  256. exit (-1)
  257. if len(sys.argv) < 2 :
  258. print ("no command specified")
  259. print_help ()
  260. exit (-1)
  261. followed_by_a_file = 0
  262. argrecognized = 0
  263. prevarg = ""
  264. input_file = ""
  265. metadata_file = ""
  266. msbs_file = ""
  267. lsbs_file = ""
  268. output_file = ""
  269. jsonstr = ""
  270. # parse all the commands
  271. for arg in sys.argv[1:]:
  272. if ( arg not in list_of_commands ) and ( arg not in list_of_options ) :
  273. argrecognized = 0
  274. else :
  275. argrecognized = 1
  276. # check command/option validity
  277. if followed_by_a_file == 0 :
  278. if argrecognized == 0:
  279. print ("argument {} not recognized".format(arg))
  280. print_help ()
  281. exit (-1)
  282. arglist[ list_of_everything.index(arg) ] += 1
  283. if arglist[ list_of_everything.index(arg) ] > 1 :
  284. print ("command {} repeted too many times".format(arg))
  285. print_help ()
  286. exit (-1)
  287. if ( arg == source_str ) or ( arg == destination_str ) or ( arg == msbs_str ) or ( arg == lsbs_str ) or ( arg == metadata_str ) :
  288. followed_by_a_file = 1
  289. else :
  290. if followed_by_a_file == 1 :
  291. if argrecognized == 1 :
  292. print ("argument {} should be followed by a file".format(prevarg))
  293. print_help ()
  294. exit (-1)
  295. else :
  296. if prevarg == msbs_str :
  297. msbs_file = arg
  298. elif prevarg == lsbs_str :
  299. lsbs_file = arg
  300. elif prevarg == metadata_str :
  301. metadata_file = arg
  302. elif prevarg == source_str :
  303. input_file = arg
  304. elif prevarg == destination_str :
  305. output_file = arg
  306. followed_by_a_file = 0
  307. prevarg = arg
  308. # common checks
  309. if followed_by_a_file == 1 :
  310. print ("string or file not specified")
  311. print_help ()
  312. exit (-1)
  313. if ( ( arglist[ list_of_everything.index(split_str)] == 0 ) and \
  314. ( arglist[ list_of_everything.index(join_str)] == 0 )) :
  315. print ("at least on of the following commands should be specified {} {}".format( join_str, split_str))
  316. print_help ()
  317. exit (-1)
  318. if ( arglist[ list_of_everything.index(split_str)] == 1 ) and \
  319. ( arglist[ list_of_everything.index(join_str)] == 1 ) :
  320. print ("argument {} should not be specified whith {}".format(split_str,join_str))
  321. print_help ()
  322. exit (-1)
  323. if ( arglist[ list_of_everything.index(destination_str)] == 1 ) and \
  324. ( arglist[ list_of_everything.index(source_str)] == 1 ) :
  325. print ("argument {} should not be specified whith {}".format(destination_str,source_str))
  326. print_help ()
  327. exit (-1)
  328. if ( arglist[ list_of_everything.index(split_str)] == 1 ) and \
  329. (( arglist[ list_of_everything.index(msbs_str)] == 0 ) or \
  330. ( arglist[ list_of_everything.index(lsbs_str)] == 0 ) or \
  331. ( arglist[ list_of_everything.index(metadata_str)] == 0 ) or \
  332. ( arglist[ list_of_everything.index(source_str)] == 0 )) :
  333. print ("wrong {} command".format(split_str))
  334. print_help ()
  335. exit (-1)
  336. if ( arglist[ list_of_everything.index(join_str)] == 1 ) and \
  337. (( arglist[ list_of_everything.index(msbs_str)] == 0 ) or \
  338. ( arglist[ list_of_everything.index(lsbs_str)] == 0 ) or \
  339. ( arglist[ list_of_everything.index(metadata_str)] == 0 ) or \
  340. ( arglist[ list_of_everything.index(destination_str)] == 0 )) :
  341. print ("wrong {} command".format(join_str))
  342. print_help ()
  343. exit (-1)
  344. ###############
  345. # Run command #
  346. ###############
  347. if ( arglist[ list_of_everything.index(split_str)] == 1 ) :
  348. _ , file_extension = os.path.splitext(input_file)
  349. if ( file_extension == ".png" ) or ( file_extension == ".PNG" ) :
  350. png_to_three_arrays(input_file, metadata_file, msbs_file, lsbs_file)
  351. if ( file_extension == ".flac" ) or ( file_extension == ".FLAC" ) :
  352. flac_to_three_arrays(input_file, metadata_file, msbs_file, lsbs_file)
  353. if ( arglist[ list_of_everything.index(join_str)] == 1 ) :
  354. _ , file_extension_str = os.path.splitext(output_file)
  355. if ( file_extension_str == ".png" ) or ( file_extension_str == ".PNG" ) :
  356. file_extension = PNG
  357. elif ( file_extension_str == ".flac" ) or ( file_extension_str == ".FLAC" ) :
  358. file_extension = FLAC
  359. three_arrays_to_file( metadata_file, msbs_file, lsbs_file, output_file, file_extension )