diff --git a/src/scratchtocatrobat/converter/converter.py b/src/scratchtocatrobat/converter/converter.py index 1ef85992..bff832f1 100644 --- a/src/scratchtocatrobat/converter/converter.py +++ b/src/scratchtocatrobat/converter/converter.py @@ -108,7 +108,7 @@ def _placeholder_for_unmapped_blocks_to(*args): return catbricks.NoteBrick(UNSUPPORTED_SCRATCH_BLOCK_NOTE_MESSAGE_PREFIX_TEMPLATE.format(_arguments_string(args))) def _key_to_broadcast_message(key_name): - return "key " + key_name + " pressed" + return "key_" + key_name + "_pressed" def _get_existing_sprite_with_name(sprite_list, name): for sprite in sprite_list: @@ -578,16 +578,16 @@ def _key_filename_for(key): assert key is not None key_path = _key_image_path_for(key) # TODO: extract method, already used once - return common.md5_hash(key_path) + "_" + _key_to_broadcast_message(key) + os.path.splitext(key_path)[1] + key_name = _key_to_broadcast_message(key).replace(" ", "_") + _, ext = os.path.splitext(key_path) + return key_name + ext -def _generate_mouse_filename(): - mouse_path = _mouse_image_path() - return common.md5_hash(mouse_path) + "_" + MOUSE_SPRITE_FILENAME +def _get_mouse_filename(): + return MOUSE_SPRITE_FILENAME def generated_variable_name(variable_name): return _GENERATED_VARIABLE_PREFIX + variable_name - def _sound_length_variable_name_for(resource_name): return generated_variable_name(_SOUND_LENGTH_VARIABLE_NAME_FORMAT.format(resource_name)) @@ -669,8 +669,10 @@ def _add_global_user_lists_to(self, catrobat_scene): catrobat_scene.project.userLists.add(global_user_list) def _add_converted_sprites_to(self, catrobat_scene): + # avoid duplicate filenames -> extend with unique identifier + duplicate_filename_set = set() for scratch_object in self.scratch_project.objects: - catr_sprite = self._scratch_object_converter(scratch_object) + catr_sprite = self._scratch_object_converter(scratch_object, duplicate_filename_set) catrobat_scene.addSprite(catr_sprite) def add_cursor_sprite_to(self, catrobat_scene, upcoming_sprites): @@ -685,7 +687,7 @@ def add_cursor_sprite_to(self, catrobat_scene, upcoming_sprites): look = catcommon.LookData() look.setName(MOUSE_SPRITE_NAME) - mouse_filename = _generate_mouse_filename() + mouse_filename = _get_mouse_filename() look.fileName = mouse_filename sprite.getLookList().add(look) @@ -1017,10 +1019,10 @@ def __init__(self, catrobat_project, scratch_project, progress_bar=None, context self._progress_bar = progress_bar self._context = context - def __call__(self, scratch_object): - return self._catrobat_sprite_from(scratch_object) + def __call__(self, scratch_object, duplicate_filename_set): + return self._catrobat_sprite_from(scratch_object, duplicate_filename_set) - def _catrobat_sprite_from(self, scratch_object): + def _catrobat_sprite_from(self, scratch_object, duplicate_filename_set): if not isinstance(scratch_object, scratch.Object): raise common.ScratchtobatError("Input must be of type={}, but is={}".format(scratch.Object, type(scratch_object))) sprite_name = scratch_object.name @@ -1054,10 +1056,10 @@ def _catrobat_sprite_from(self, scratch_object): costume_resolution = current_costume_resolution elif current_costume_resolution != costume_resolution: log.warning("Costume resolution not same for all costumes") - sprite_looks.add(self._catrobat_look_from(scratch_costume)) + sprite_looks.add(self._catrobat_look_from(scratch_costume, duplicate_filename_set)) sprite_sounds = sprite.getSoundList() for scratch_sound in scratch_object.get_sounds(): - sprite_sounds.add(self._catrobat_sound_from(scratch_sound)) + sprite_sounds.add(self._catrobat_sound_from(scratch_sound, duplicate_filename_set)) if not scratch_object.is_stage() and scratch_object.get_lists() is not None: for user_list_data in scratch_object.get_lists(): @@ -1110,7 +1112,7 @@ def _catrobat_sprite_from(self, scratch_object): return sprite @staticmethod - def _catrobat_look_from(scratch_costume): + def _catrobat_look_from(scratch_costume, duplicate_filename_set): if not scratch_costume or not (isinstance(scratch_costume, dict) and all(_ in scratch_costume for _ in (scratchkeys.COSTUME_MD5, scratchkeys.COSTUME_NAME))): raise common.ScratchtobatError("Wrong input, must be costume dict: {}".format(scratch_costume)) look = catcommon.LookData() @@ -1121,12 +1123,11 @@ def _catrobat_look_from(scratch_costume): assert scratchkeys.COSTUME_MD5 in scratch_costume costume_md5_filename = scratch_costume[scratchkeys.COSTUME_MD5] - costume_resource_name = scratch_costume[scratchkeys.COSTUME_NAME] - look.fileName = (mediaconverter.catrobat_resource_file_name_for(costume_md5_filename, costume_resource_name)) + look.fileName = helpers.create_catrobat_md5_filename(costume_md5_filename, duplicate_filename_set) return look @staticmethod - def _catrobat_sound_from(scratch_sound): + def _catrobat_sound_from(scratch_sound, duplicate_filename_set): soundinfo = catcommon.SoundInfo() assert scratchkeys.SOUND_NAME in scratch_sound @@ -1135,8 +1136,7 @@ def _catrobat_sound_from(scratch_sound): assert scratchkeys.SOUND_MD5 in scratch_sound sound_md5_filename = scratch_sound[scratchkeys.SOUND_MD5] - sound_resource_name = scratch_sound[scratchkeys.SOUND_NAME] - soundinfo.fileName = (mediaconverter.catrobat_resource_file_name_for(sound_md5_filename, sound_resource_name)) + soundinfo.fileName = helpers.create_catrobat_md5_filename(sound_md5_filename, duplicate_filename_set) return soundinfo @staticmethod @@ -1403,7 +1403,7 @@ def write_program_source(catrobat_program, context): for sprite in catrobat_program.getDefaultScene().spriteList: if sprite.name == MOUSE_SPRITE_NAME: mouse_img_path = _mouse_image_path() - shutil.copyfile(mouse_img_path, os.path.join(images_path, _generate_mouse_filename())) + shutil.copyfile(mouse_img_path, os.path.join(images_path, _get_mouse_filename())) break def download_automatic_screenshot_if_available(output_dir, scratch_project): diff --git a/src/scratchtocatrobat/converter/mediaconverter.py b/src/scratchtocatrobat/converter/mediaconverter.py index 31495626..d1ce9975 100644 --- a/src/scratchtocatrobat/converter/mediaconverter.py +++ b/src/scratchtocatrobat/converter/mediaconverter.py @@ -48,24 +48,6 @@ class MediaType(object): UNCONVERTED_WAV = 4 -def catrobat_resource_file_name_for(scratch_md5_name, scratch_resource_name): - assert os.path.basename(scratch_md5_name) == scratch_md5_name \ - and len(os.path.splitext(scratch_md5_name)[0]) == 32, \ - "Must be MD5 hash with file ext: " + scratch_md5_name - - # remove unsupported unicode characters from filename -# if isinstance(scratch_resource_name, unicode): -# scratch_resource_name = unicodedata.normalize('NFKD', scratch_resource_name).encode('ascii','ignore') -# if (scratch_resource_name == None) or (len(scratch_resource_name) == 0): -# scratch_resource_name = "unicode_replaced" - resource_ext = os.path.splitext(scratch_md5_name)[1] - return scratch_md5_name.replace(resource_ext, "_" + scratch_resource_name.replace("/",'') + resource_ext) - - -def _resource_name_for(file_path): - return common.md5_hash(file_path) + os.path.splitext(file_path)[1] - - class _MediaResourceConverterThread(Thread): def run(self): @@ -97,7 +79,7 @@ def __init__(self, scratch_project, catrobat_program, images_path, sounds_path): self.catrobat_program = catrobat_program self.images_path = images_path self.sounds_path = sounds_path - self.renamed_files_map = {} + self.file_rename_map = {} def convert(self, progress_bar = None): @@ -215,8 +197,10 @@ def convert(self, progress_bar = None): assert reference_index == resource_index and reference_index == num_total_resources converted_media_files_to_be_removed = set() + duplicate_filename_set = set() for resource_info in all_used_resources: - scratch_md5_name = resource_info["scratch_md5_name"] + # reconstruct the temporary catrobat filenames -> catrobat.media_objects_in(self.catrobat_file) + current_filename = helpers.create_catrobat_md5_filename(resource_info["scratch_md5_name"], duplicate_filename_set) # check if path changed after conversion old_src_path = resource_info["src_path"] @@ -258,39 +242,51 @@ def convert(self, progress_bar = None): # TODO: move test_converter.py to converter-python-package... image_processing.save_editable_image_as_png_to_disk(editable_image, image_file_path, overwrite=True) - self._copy_media_file(scratch_md5_name, src_path, resource_info["dest_path"], - resource_info["media_type"]) + current_basename, _ = os.path.splitext(current_filename) + self.file_rename_map[current_basename] = {} + self.file_rename_map[current_basename]["src_path"] = src_path + self.file_rename_map[current_basename]["dst_path"] = resource_info["dest_path"] + self.file_rename_map[current_basename]["media_type"] = resource_info["media_type"] if resource_info["media_type"] in { MediaType.UNCONVERTED_SVG, MediaType.UNCONVERTED_WAV }: converted_media_files_to_be_removed.add(src_path) - self._update_file_names_of_converted_media_files() + self.rename_media_files_and_copy() + # delete converted png files -> only temporary saved for media_file_to_be_removed in converted_media_files_to_be_removed: os.remove(media_file_to_be_removed) + # rename the media files and copy them to the catrobat project directory + def rename_media_files_and_copy(self): - def _update_file_names_of_converted_media_files(self): - for (old_file_name, new_file_name) in self.renamed_files_map.iteritems(): - look_data_or_sound_infos = filter(lambda info: info.fileName == old_file_name, - catrobat.media_objects_in(self.catrobat_program)) - # assert len(look_data_or_sound_infos) > 0 + def create_new_file_name(provided_file, index_helper, file_type): + _, ext = os.path.splitext(provided_file) + if file_type in {MediaType.UNCONVERTED_SVG, MediaType.IMAGE}: + return "img_#" + str(index_helper.assign_image_index()) + ext + else: + return "snd_#" + str(index_helper.assign_sound_index()) + ext + + media_file_index_helper = helpers.MediaFileIndex() + for info in catrobat.media_objects_in(self.catrobat_program): + basename, _ = os.path.splitext(info.fileName) + + # ignore these files, already correctly provided by the converter + if any(x in basename for x in ["key", "mouse"]): + continue - for info in look_data_or_sound_infos: - info.fileName = new_file_name + assert basename in self.file_rename_map and \ + "src_path" in self.file_rename_map[basename] and \ + "dst_path" in self.file_rename_map[basename] and \ + "media_type" in self.file_rename_map[basename] - def _copy_media_file(self, scratch_md5_name, src_path, dest_path, media_type): - # for Catrobat separate file is needed for resources which are used multiple times but with different names - for scratch_resource_name in self.scratch_project.find_all_resource_names_for(scratch_md5_name): - new_file_name = catrobat_resource_file_name_for(scratch_md5_name, scratch_resource_name) - if media_type in { MediaType.UNCONVERTED_SVG, MediaType.UNCONVERTED_WAV }: - old_file_name = new_file_name - converted_scratch_md5_name = _resource_name_for(src_path) - new_file_name = catrobat_resource_file_name_for(converted_scratch_md5_name, - scratch_resource_name) - self.renamed_files_map[old_file_name] = new_file_name - shutil.copyfile(src_path, os.path.join(dest_path, new_file_name)) + src_path = self.file_rename_map[basename]["src_path"] + dst_path = self.file_rename_map[basename]["dst_path"] + media_type = self.file_rename_map[basename]["media_type"] + new_file_name = create_new_file_name(src_path, media_file_index_helper, media_type) + shutil.copyfile(src_path, os.path.join(dst_path, new_file_name)) + info.fileName = new_file_name def resize_png(self, path_in, path_out, bitmapResolution): import java.awt.image.BufferedImage diff --git a/src/scratchtocatrobat/converter/test_converter.py b/src/scratchtocatrobat/converter/test_converter.py index e8e46acf..627b9f50 100644 --- a/src/scratchtocatrobat/converter/test_converter.py +++ b/src/scratchtocatrobat/converter/test_converter.py @@ -2511,7 +2511,7 @@ def test_key_pressed_block(self): key_w = default_scene.spriteList[2] assert key_w != None - assert key_w.name == 'key w pressed' + assert key_w.name == 'key_w_pressed' scripts = key_w.getScriptList() assert len(scripts) == 3 @@ -2573,7 +2573,7 @@ def test_key_pressed_script(self): broadcast_script = spritescripts[1] assert isinstance(broadcast_script, catbase.BroadcastScript) - assert broadcast_script.getBroadcastMessage() == 'key space pressed' + assert broadcast_script.getBroadcastMessage() == 'key_space_pressed' sprite_brick_list = spritescripts[1].getBrickList() assert len(sprite_brick_list) == 1 @@ -2583,7 +2583,7 @@ def test_key_pressed_script(self): key_space = default_scene.spriteList[2] assert key_space != None - assert key_space.name == 'key space pressed' + assert key_space.name == 'key_space_pressed' keyscripts = key_space.getScriptList() assert len(keyscripts) == 2 @@ -2594,7 +2594,7 @@ def test_key_pressed_script(self): broadcast_brick = key_brick_list[0] assert isinstance(broadcast_brick, catbricks.BroadcastBrick) - assert broadcast_brick.getBroadcastMessage() == 'key space pressed' + assert broadcast_brick.getBroadcastMessage() == 'key_space_pressed' diff --git a/src/scratchtocatrobat/tools/helpers.py b/src/scratchtocatrobat/tools/helpers.py index cb599954..b0067b0a 100644 --- a/src/scratchtocatrobat/tools/helpers.py +++ b/src/scratchtocatrobat/tools/helpers.py @@ -465,3 +465,39 @@ def update(self, progress_type, increment=1): self._output_stream.write("{}{}{}\n".format(ProgressBar.START_PROGRESS_INDICATOR, \ round(percentage, 2), \ ProgressBar.END_PROGRESS_INDICATOR)) + + +# create a unique name for a catrobat media file -> name derived from scratch md5-hash +# since one file might be used for multiple objects, the md5-hash is extended with a unique identifier +# name used until all files, converted or unconverted, are copied to the catrobat project directory +def create_catrobat_md5_filename(scratch_md5_name, duplicate_file_set): + filename, ext = os.path.splitext(scratch_md5_name) + current_filename = filename + "_#0" + ext + next_index = 1 + while current_filename in duplicate_file_set: + current_filename = filename + "_#" + str(next_index) + ext + next_index += 1 + duplicate_file_set.add(current_filename) + return current_filename + + +class MediaFileIndex: + def __init__(self): + self.img_idx = 0 + self.snd_idx = 0 + + def increment_image_index(self): + self.img_idx += 1 + + def increment_sound_index(self): + self.snd_idx += 1 + + def assign_image_index(self): + current_image_index = self.img_idx + self.increment_image_index() + return current_image_index + + def assign_sound_index(self): + current_sound_index = self.snd_idx + self.increment_sound_index() + return current_sound_index