From f4a73c03f1dd65060c6ebac95327bf418dd97fc3 Mon Sep 17 00:00:00 2001 From: Tom Hammel Date: Fri, 21 Nov 2025 11:24:28 +0100 Subject: [PATCH 1/6] adapter.aasx: Add support for loading and saving thumbnails in AASX package Previously, the `AASXReader` did not load the thumbnail in the `read_into` function, instead a separate function needed to be called. This behavior did also occur in `AASXWriter` with writing the thumbnail. This was unintuitive, as it required a separat call of the load/save function. This PR implements the loading/storing of the thumbnail directly in the loading/storing of other supplementary files. Fixes #435 --- sdk/basyx/aas/adapter/aasx.py | 87 ++++++++++++++++++++++++----------- 1 file changed, 59 insertions(+), 28 deletions(-) diff --git a/sdk/basyx/aas/adapter/aasx.py b/sdk/basyx/aas/adapter/aasx.py index 8bb5958f..cb2bfe0c 100644 --- a/sdk/basyx/aas/adapter/aasx.py +++ b/sdk/basyx/aas/adapter/aasx.py @@ -231,6 +231,8 @@ def _read_aas_part_into(self, part_name: str, read_identifiables.add(obj.id) if isinstance(obj, model.Submodel): self._collect_supplementary_files(part_name, obj, file_store) + elif isinstance(obj, model.AssetAdministrationShell): + self._collect_supplementary_files(part_name, obj, file_store) def _parse_aas_part(self, part_name: str, **kwargs) -> model.DictObjectStore: """ @@ -261,33 +263,55 @@ def _parse_aas_part(self, part_name: str, **kwargs) -> model.DictObjectStore: raise ValueError(error_message) return model.DictObjectStore() - def _collect_supplementary_files(self, part_name: str, submodel: model.Submodel, + def _collect_supplementary_files(self, part_name: str, root_element: Union[model.AssetAdministrationShell, model.Submodel], file_store: "AbstractSupplementaryFileContainer") -> None: """ - Helper function to search File objects within a single parsed Submodel, extract the referenced supplementary - files and update the File object's values with the absolute path. + Helper function to search File objects within a single parsed AssetAdministrationShell or Submodel. + Resolve their absolute paths, and update the corresponding File/Thumbnail objects with the absolute path. - :param part_name: The OPC part name of the part the Submodel has been parsed from. This is used to resolve + :param part_name: The OPC part name of the part the root_element has been parsed from. This is used to resolve relative file paths. - :param submodel: The Submodel to process + :param root_element: The AssetAdministrationShell or Submodel to process :param file_store: The SupplementaryFileContainer to add the extracted supplementary files to """ - for element in traversal.walk_submodel(submodel): - if isinstance(element, model.File): - if element.value is None: - continue - # Only absolute-path references and relative-path URI references (see RFC 3986, sec. 4.2) are considered - # to refer to files within the AASX package. Thus, we must skip all other types of URIs (esp. absolute - # URIs and network-path references) - if element.value.startswith('//') or ':' in element.value.split('/')[0]: - logger.info(f"Skipping supplementary file {element.value}, since it seems to be an absolute URI or " - f"network-path URI reference") - continue - absolute_name = pyecma376_2.package_model.part_realpath(element.value, part_name) - logger.debug(f"Reading supplementary file {absolute_name} from AASX package ...") - with self.reader.open_part(absolute_name) as p: - final_name = file_store.add_file(absolute_name, p, self.reader.get_content_type(absolute_name)) - element.value = final_name + if isinstance(root_element, model.AssetAdministrationShell): + if (root_element.asset_information.default_thumbnail and + root_element.asset_information.default_thumbnail.path): + file_name = self._add_supplementary_file(part_name, root_element.asset_information.default_thumbnail.path, file_store) + if file_name: + root_element.asset_information.default_thumbnail.path = file_name + if isinstance(root_element, model.Submodel): + for element in traversal.walk_submodel(root_element): + if isinstance(element, model.File): + if element.value is None: + continue + final_name = self._add_supplementary_file(part_name, element.value, file_store) + if final_name: + element.value = final_name + + def _add_supplementary_file(self, part_name: str, file_path: str, + file_store: "AbstractSupplementaryFileContainer") -> Union[str, None]: + """ + Helper function to extract a single referenced supplementary file and return the absolute path within the AASX package. + + :param part_name: The OPC part name of the part the root_element has been parsed from. This is used to resolve + relative file paths. + :param file_path: The file path or URI reference of the supplementary file to be extracted + :param file_store: The SupplementaryFileContainer to add the extracted supplementary files to + :return: The stored file name as returned by *file_store*, or ``None`` if the reference was skipped. + """ + # Only absolute-path references and relative-path URI references (see RFC 3986, sec. 4.2) are considered + # to refer to files within the AASX package. Thus, we must skip all other types of URIs (esp. absolute + # URIs and network-path references) + if file_path.startswith('//') or ':' in file_path.split('/')[0]: + logger.info(f"Skipping supplementary file {file_path}, since it seems to be an absolute URI or network-path URI reference") + return None + absolute_name = pyecma376_2.package_model.part_realpath(file_path, part_name) + logger.debug(f"Reading supplementary file {absolute_name} from AASX package ...") + with self.reader.open_part(absolute_name) as p: + final_name = file_store.add_file(absolute_name, p, + self.reader.get_content_type(absolute_name)) + return final_name class AASXWriter: @@ -541,7 +565,8 @@ def write_all_aas_objects(self, contained objects into an ``aas_env`` part in the AASX package. If the ObjectStore includes :class:`~basyx.aas.model.submodel.Submodel` objects, supplementary files which are referenced by :class:`~basyx.aas.model.submodel.File` objects within those Submodels, are fetched from the ``file_store`` - and added to the AASX package. + and added to the AASX package. If the ObjectStore contains a thumbnail referenced by + :class:`~basyx.aas.model.asset.AssetInformation.default_thumbnail`, it is also added to the AASX package. .. attention:: @@ -563,17 +588,23 @@ def write_all_aas_objects(self, logger.debug(f"Writing AASX part {part_name} with AAS objects ...") supplementary_files: List[str] = [] + def _collect_supplementary_file(file_name: str) -> None: + # Skip File objects with empty value URI references that are considered to be no local file + # (absolute URIs or network-path URI references) + if file_name is None or file_name.startswith('//') or ':' in file_name.split('/')[0]: + return + supplementary_files.append(file_name) + # Retrieve objects and scan for referenced supplementary files for the_object in objects: + if isinstance(the_object, model.AssetAdministrationShell): + if (the_object.asset_information.default_thumbnail and + the_object.asset_information.default_thumbnail.path): + _collect_supplementary_file(the_object.asset_information.default_thumbnail.path) if isinstance(the_object, model.Submodel): for element in traversal.walk_submodel(the_object): if isinstance(element, model.File): - file_name = element.value - # Skip File objects with empty value URI references that are considered to be no local file - # (absolute URIs or network-path URI references) - if file_name is None or file_name.startswith('//') or ':' in file_name.split('/')[0]: - continue - supplementary_files.append(file_name) + _collect_supplementary_file(element.value) # Add aas-spec relationship if not split_part: From 1aaa7db9aecefbf564d6a9819ce0cdb43bce1273 Mon Sep 17 00:00:00 2001 From: s-heppner Date: Tue, 2 Dec 2025 11:26:32 +0100 Subject: [PATCH 2/6] Update sdk/basyx/aas/adapter/aasx.py --- sdk/basyx/aas/adapter/aasx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/basyx/aas/adapter/aasx.py b/sdk/basyx/aas/adapter/aasx.py index cb2bfe0c..8513966e 100644 --- a/sdk/basyx/aas/adapter/aasx.py +++ b/sdk/basyx/aas/adapter/aasx.py @@ -290,7 +290,7 @@ def _collect_supplementary_files(self, part_name: str, root_element: Union[model element.value = final_name def _add_supplementary_file(self, part_name: str, file_path: str, - file_store: "AbstractSupplementaryFileContainer") -> Union[str, None]: + file_store: "AbstractSupplementaryFileContainer") -> Optional[str]: """ Helper function to extract a single referenced supplementary file and return the absolute path within the AASX package. From 438c288564bc28481b956d51fbe261ae538c1e88 Mon Sep 17 00:00:00 2001 From: s-heppner Date: Tue, 2 Dec 2025 11:31:22 +0100 Subject: [PATCH 3/6] Apply suggestion from @s-heppner --- sdk/basyx/aas/adapter/aasx.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sdk/basyx/aas/adapter/aasx.py b/sdk/basyx/aas/adapter/aasx.py index 8513966e..c3674e48 100644 --- a/sdk/basyx/aas/adapter/aasx.py +++ b/sdk/basyx/aas/adapter/aasx.py @@ -604,7 +604,8 @@ def _collect_supplementary_file(file_name: str) -> None: if isinstance(the_object, model.Submodel): for element in traversal.walk_submodel(the_object): if isinstance(element, model.File): - _collect_supplementary_file(element.value) + if element.value: + _collect_supplementary_file(element.value) # Add aas-spec relationship if not split_part: From dde891f6da402693d0200f540c67dcfc2225de99 Mon Sep 17 00:00:00 2001 From: s-heppner Date: Tue, 2 Dec 2025 11:36:07 +0100 Subject: [PATCH 4/6] Apply suggestion from @s-heppner --- sdk/basyx/aas/adapter/aasx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/basyx/aas/adapter/aasx.py b/sdk/basyx/aas/adapter/aasx.py index c3674e48..cedd5c76 100644 --- a/sdk/basyx/aas/adapter/aasx.py +++ b/sdk/basyx/aas/adapter/aasx.py @@ -566,7 +566,7 @@ def write_all_aas_objects(self, :class:`~basyx.aas.model.submodel.Submodel` objects, supplementary files which are referenced by :class:`~basyx.aas.model.submodel.File` objects within those Submodels, are fetched from the ``file_store`` and added to the AASX package. If the ObjectStore contains a thumbnail referenced by - :class:`~basyx.aas.model.asset.AssetInformation.default_thumbnail`, it is also added to the AASX package. + :class:`~basyx.aas.model.aas.AssetInformation.default_thumbnail`, it is also added to the AASX package. .. attention:: From b17f4f809c8fdc10e995fce582777595f78c1f91 Mon Sep 17 00:00:00 2001 From: Tom Hammel Date: Tue, 9 Dec 2025 16:27:03 +0100 Subject: [PATCH 5/6] Fix wrong path reference and too long line errors --- sdk/basyx/aas/adapter/aasx.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/sdk/basyx/aas/adapter/aasx.py b/sdk/basyx/aas/adapter/aasx.py index cedd5c76..5cb972e2 100644 --- a/sdk/basyx/aas/adapter/aasx.py +++ b/sdk/basyx/aas/adapter/aasx.py @@ -263,7 +263,8 @@ def _parse_aas_part(self, part_name: str, **kwargs) -> model.DictObjectStore: raise ValueError(error_message) return model.DictObjectStore() - def _collect_supplementary_files(self, part_name: str, root_element: Union[model.AssetAdministrationShell, model.Submodel], + def _collect_supplementary_files(self, part_name: str, + root_element: Union[model.AssetAdministrationShell, model.Submodel], file_store: "AbstractSupplementaryFileContainer") -> None: """ Helper function to search File objects within a single parsed AssetAdministrationShell or Submodel. @@ -277,7 +278,9 @@ def _collect_supplementary_files(self, part_name: str, root_element: Union[model if isinstance(root_element, model.AssetAdministrationShell): if (root_element.asset_information.default_thumbnail and root_element.asset_information.default_thumbnail.path): - file_name = self._add_supplementary_file(part_name, root_element.asset_information.default_thumbnail.path, file_store) + file_name = self._add_supplementary_file(part_name, + root_element.asset_information.default_thumbnail.path, + file_store) if file_name: root_element.asset_information.default_thumbnail.path = file_name if isinstance(root_element, model.Submodel): @@ -292,7 +295,8 @@ def _collect_supplementary_files(self, part_name: str, root_element: Union[model def _add_supplementary_file(self, part_name: str, file_path: str, file_store: "AbstractSupplementaryFileContainer") -> Optional[str]: """ - Helper function to extract a single referenced supplementary file and return the absolute path within the AASX package. + Helper function to extract a single referenced supplementary file + and return the absolute path within the AASX package. :param part_name: The OPC part name of the part the root_element has been parsed from. This is used to resolve relative file paths. @@ -304,13 +308,13 @@ def _add_supplementary_file(self, part_name: str, file_path: str, # to refer to files within the AASX package. Thus, we must skip all other types of URIs (esp. absolute # URIs and network-path references) if file_path.startswith('//') or ':' in file_path.split('/')[0]: - logger.info(f"Skipping supplementary file {file_path}, since it seems to be an absolute URI or network-path URI reference") + logger.info(f"Skipping supplementary file {file_path}, since it seems to be an absolute URI or " + f"network-path URI reference") return None absolute_name = pyecma376_2.package_model.part_realpath(file_path, part_name) logger.debug(f"Reading supplementary file {absolute_name} from AASX package ...") with self.reader.open_part(absolute_name) as p: - final_name = file_store.add_file(absolute_name, p, - self.reader.get_content_type(absolute_name)) + final_name = file_store.add_file(absolute_name, p, self.reader.get_content_type(absolute_name)) return final_name @@ -566,7 +570,7 @@ def write_all_aas_objects(self, :class:`~basyx.aas.model.submodel.Submodel` objects, supplementary files which are referenced by :class:`~basyx.aas.model.submodel.File` objects within those Submodels, are fetched from the ``file_store`` and added to the AASX package. If the ObjectStore contains a thumbnail referenced by - :class:`~basyx.aas.model.aas.AssetInformation.default_thumbnail`, it is also added to the AASX package. + :attr:`~basyx.aas.model.aas.AssetInformation.default_thumbnail`, it is also added to the AASX package. .. attention:: From 4f7929952f7ca22b57ab56e1cc82847b3ac2244f Mon Sep 17 00:00:00 2001 From: Igor Garmaev <56840636+zrgt@users.noreply.github.com> Date: Wed, 10 Dec 2025 15:39:46 +0100 Subject: [PATCH 6/6] Fix docs --- sdk/basyx/aas/adapter/aasx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/basyx/aas/adapter/aasx.py b/sdk/basyx/aas/adapter/aasx.py index 5cb972e2..ebd1273a 100644 --- a/sdk/basyx/aas/adapter/aasx.py +++ b/sdk/basyx/aas/adapter/aasx.py @@ -570,7 +570,7 @@ def write_all_aas_objects(self, :class:`~basyx.aas.model.submodel.Submodel` objects, supplementary files which are referenced by :class:`~basyx.aas.model.submodel.File` objects within those Submodels, are fetched from the ``file_store`` and added to the AASX package. If the ObjectStore contains a thumbnail referenced by - :attr:`~basyx.aas.model.aas.AssetInformation.default_thumbnail`, it is also added to the AASX package. + ``default_thumbnail`` in :class:`~basyx.aas.model.aas.AssetInformation`, it is also added to the AASX package. .. attention::