2@brief Utilities to extract materials from the GPUOpen material database. This is not a complete set of calls to extract out all material information but instead enough to find materials
3and extract out specific packages from the list of available materials.
5See: https://api.matlib.gpuopen.com/api/swagger/ for information on available API calls.
8import requests, json, os, io, re, zipfile, logging
9from http
import HTTPStatus
16from PIL
import Image
as PILImage
21 This class is used to load materials from the GPUOpen material database.
22 See: https://api.matlib.gpuopen.com/api/swagger/ for API information.
26 Initialize the GPUOpen material loader.
29 self.
root_url =
'https://api.matlib.gpuopen.com/api'
46 self.
logger = logging.getLogger(
'GPUO')
47 logging.basicConfig(level=logging.INFO)
51 Write a package data to a file.
52 @param data: The data to write.
53 @param outputFolder: The output folder to write the file to.
54 @param title: The title of the file.
55 @param url: The URL for the material preview image.
56 @param unzipFile: If true, the file is unzipped to a folder with the same name
58 @return: True if the package was written.
63 if not os.path.exists(outputFolder):
64 os.makedirs(outputFolder)
68 unzipFolder = os.path.join(outputFolder, title)
71 with io.BytesIO(data)
as data_io:
72 with zipfile.ZipFile(data_io,
'r')
as zip_ref:
73 zip_ref.extractall(unzipFolder)
76 urlFile = os.path.join(unzipFolder,
'url.txt')
77 with open(urlFile,
'w')
as f:
80 self.
logger.info(f
'Unzipped to folder: "{unzipFolder}"')
84 zip_buffer = io.BytesIO()
86 with zipfile.ZipFile(io.BytesIO(data),
'a', zipfile.ZIP_DEFLATED)
as zip_in:
88 with zipfile.ZipFile(zip_buffer,
'w', zipfile.ZIP_DEFLATED)
as zip_out:
89 for item
in zip_in.infolist():
90 zip_out.writestr(item, zip_in.read(item.filename))
92 zip_out.writestr(
'url.txt', url)
93 data = zip_buffer.getvalue()
95 outputFile = os.path.join(outputFolder, f
"{title}.zip")
96 with open(outputFile,
"wb")
as f:
97 self.
logger.info(f
'Write package to file: "{outputFile}"')
105 Convert a PIL image to a Base64 string.
106 @param image: An instance of PIL.Image
107 @return: Base64-encoded string of the image
111 self.
logger.debug(
'Pillow (PIL) image module not provided. Image data will not be converted to Base64.')
114 self.
logger.debug(
'No image data. Image data will not be converted to Base64.')
122 image.save(buffer, format=
"PNG")
123 binary_data = buffer.getvalue()
124 base64_encoded_data = base64.b64encode(binary_data).decode(
'utf-8')
127 return base64_encoded_data
131 Check that the images in the zip file meet the USDZ format restrictions.
132 @param zip_data: The binary data of the zip file.
133 @param pilImage: The PIL image module.
134 @return: A list of issues found with the images. If the list is empty, then no issues were found.
136 ZIP_COMPRESSION_NAMES = {
137 zipfile.ZIP_STORED:
"ZIP_STORED",
138 zipfile.ZIP_DEFLATED:
"ZIP_DEFLATED",
139 zipfile.ZIP_BZIP2:
"ZIP_BZIP2",
140 zipfile.ZIP_LZMA:
"ZIP_LZMA"
144 with zipfile.ZipFile(io.BytesIO(zip_data),
'r')
as zf:
145 for info
in zf.infolist():
147 if info.compress_type != zipfile.ZIP_STORED:
148 compression_name = ZIP_COMPRESSION_NAMES.get(info.compress_type, str(info.compress_type))
149 issues.append(f
"{info.filename} is not uncompressed (ZIP_STORED). {compression_name} found.")
151 if info.filename.lower().endswith((
'.png',
'.jpg',
'.jpeg')):
152 img_data = zf.read(info.filename)
154 img = pilImage.open(io.BytesIO(img_data))
155 if img.format
not in (
'PNG',
'JPEG'):
156 issues.append(f
"{info.filename} is not PNG or JPEG")
157 if img.format ==
'PNG':
158 if img.info.get(
'interlace', 0) != 0:
159 issues.append(f
"{info.filename} is interlaced PNG (not allowed)")
160 if img.mode
not in (
'L',
'LA',
'RGB',
'RGBA'):
161 issues.append(f
"{info.filename} PNG mode {img.mode} not allowed")
162 if img.format ==
'JPEG':
163 if img.info.get(
'progressive', 0) != 0:
164 issues.append(f
"{info.filename} is progressive JPEG (not allowed)")
165 except Exception
as e:
166 issues.append(f
"{info.filename} could not be opened as image: {e}")
171 Extract the package data from a zip file.
172 @param data: The data to extract.
173 @param pilImage: The PIL image module.
174 @return: A list of extracted data of the form:
175 [ { 'file_name': file_name, 'data': data, 'type': type } ]
180 self.
logger.debug(
'Pillow (PIL) image module provided. Image data will not be extracted.')
182 zip_object = io.BytesIO(data)
184 extracted_data_list = []
185 with zipfile.ZipFile(zip_object,
'r')
as zip_file:
187 for file_name
in zip_file.namelist():
189 extracted_data = zip_file.read(file_name)
190 if file_name.endswith(
'.mtlx'):
191 mtlx_string = extracted_data.decode(
'utf-8')
192 extracted_data_list.append( {
'file_name': file_name,
'data': mtlx_string,
'type':
'mtlx'} )
195 elif file_name.endswith(
'.png'):
197 image = pilImage.open(io.BytesIO(extracted_data))
200 extracted_data_list.append( {
'file_name': file_name,
'data': image,
'type':
'image'} )
202 return extracted_data_list
206 Download a package for a given material from the GPUOpen material database.
207 @param listNumber: The list number of the material to download.
208 @param materialNumber: The material number to download.
209 @param packageId: The package ID to download.
210 Packages are numbered starting at 0. Default is 0.
211 with index 0 containing the smallest package (smallest resolution referenced textures).
214 self.
logger.info(
'No material loaded.')
219 self.
logger.info(f
'No material for list {listNumber}.')
224 if "results" in json_data:
225 jsonResults = json_data[
"results"]
226 if len(jsonResults) <= materialNumber:
227 self.
logger.info(f
'No material for index {materialNumber}.')
230 jsonResult = jsonResults[materialNumber]
237 if "packages" in jsonResult:
238 jsonPackages = jsonResult[
"packages"]
240 self.
logger.info(f
'No packages for material {materialNumber}.')
243 if len(jsonPackages) <= packageId:
244 self.
logger.info(f
'No package for index {packageId}.')
246 package_id = jsonPackages[packageId]
249 self.
logger.info(f
'No package for index {packageId}.')
252 url = f
"{self.package_url}/{package_id}/download"
253 data = requests.get(url).content
255 title = jsonResult[
"title"]
258 return [data, title, preview_url]
262 Download a package for a given material from the GPUOpen material database.
263 @param searchExpr: The regular expression to match the material name.
264 @param exact_match: If true, the material name must match exactly. Default is false.
265 @param packageId: The package ID to download.
266 @return: A list of downloaded packages of the form:
271 if len(foundList) > 0:
272 for found
in foundList:
273 listNumber = found[
'listNumber']
274 materialNumber = found[
'materialNumber']
275 matName = found[
'title']
276 self.
logger.info(f
'> Download material: {matName} List: {listNumber}. Index: {materialNumber}')
277 result = [data, title, url] = self.
downloadPackage(listNumber, materialNumber, packageId)
278 downloadList.append(result)
283 Find materials by name.
284 @param materialName: Regular expression to match the material name.
285 @param exact_match: If true, the material name must match exactly. Default is false.
286 @return: A list of materials that match the regular expression of the form:
287 [ { 'listNumber': listNumber, 'materialNumber': materialNumber, 'title': title } ]
296 for material
in materialList[
'results']:
298 if materialName.lower() == material[
'title'].lower():
299 materialsList.append({
'listNumber': listNumber,
'materialNumber': materialNumber,
'title': material[
'title'] })
302 if re.match(materialName, material[
'title'], re.IGNORECASE):
303 materialsList.append({
'listNumber': listNumber,
'materialNumber': materialNumber,
'title': material[
'title'] })
311 Update the material names from the material lists.
312 @return: List of material names. If no materials are loaded, then an empty list is returned.
319 for material
in materialList[
'results']:
326 @breif Given the title of a material, return the URL for the material preview image.
327 @param title: The title of the material to get the preview URL for.
328 @return: The URL for the material preview image. Else empty string.
337 if item[
'title'] == title:
338 url = item[
'preview_url']
342 def getMaterialPreviews(self, force = False) -> list | None:
349 @brief Get the material preview URLs for the materials loaded from the GPUOpen material database.
350 @return list of items of the form: { 'title': material_title, 'preview_url': url }
351 If no materials or renders are loaded, then an empty list is returned.
358 render_urls = self.
renders[
"renders"]
361 for material
in materialList[
'results']:
362 renders_order_list = material[
'renders_order']
363 material_title = material[
'title']
365 if len(renders_order_list) > 0:
367 render_lookup = renders_order_list[0]
369 for render
in render_urls:
371 if render[
'id'] == render_lookup:
372 url = render[
'thumbnail_url']
373 item = {
'title': material_title,
'preview_url': url }
378 self.
logger.info(f
'No renderings specified for material: {material_title}')
383 Get the materials returned from the GPUOpen material database.
384 Will loop based on the linked-list of materials stored in the database.
385 Currently the batch size requested is 100 materials per batch.
386 @return: List of material lists
394 'accept':
'application/json'
404 haveMoreMaterials =
True
405 while (haveMoreMaterials):
407 response = requests.get(url, headers=headers, params=params)
409 if response.status_code == HTTPStatus.OK:
411 raw_response = response.text
414 json_strings = raw_response.split(
'}{')
416 json_result_string = json_strings[0]
417 jsonObject = json.loads(json_result_string)
421 nextQuery = jsonObject[
'next']
425 queryParts = nextQuery.split(
'?')
427 queryParts = queryParts[1].split(
'&')
429 limitParts = queryParts[0].split(
'=')
430 offsetParts = queryParts[1].split(
'=')
431 params[
'limit'] = int(limitParts[1])
432 params[
'offset'] = int(offsetParts[1])
433 self.
logger.info(f
'Fetch set of materials: limit: {params["limit"]} offset: {params["offset"]}')
435 haveMoreMaterials =
False
439 self.
logger.info(f
'Error: {response.status_code}, {response.text}')
445 Get the rendering information returned from the GPUOpen material database.
446 Will loop based on the linked-list of render info stored in the database.
447 Currently the batch size requested is 100 render infos per batch.
448 @param force: If true, forces a re-download of the render information. Default is false.
449 @return: List of material lists
454 self.
renders = {
"renders": [] }
458 'accept':
'application/json'
468 haveMoreMaterials =
True
469 while (haveMoreMaterials):
471 response = requests.get(url, headers=headers, params=params)
473 if response.status_code == HTTPStatus.OK:
475 raw_response = response.text
478 json_strings = raw_response.split(
'}{')
480 json_result_string = json_strings[0]
481 jsonObject = json.loads(json_result_string)
484 results_list = jsonObject[
'results']
485 for result
in results_list:
486 self.
renders[
"renders"].append(result)
489 nextQuery = jsonObject[
'next']
493 queryParts = nextQuery.split(
'?')
495 queryParts = queryParts[1].split(
'&')
497 limitParts = queryParts[0].split(
'=')
498 offsetParts = queryParts[1].split(
'=')
499 params[
'limit'] = int(limitParts[1])
500 params[
'offset'] = int(offsetParts[1])
501 self.
logger.info(f
'Fetch set of render infos: limit: {params["limit"]} offset: {params["offset"]}')
503 haveMoreMaterials =
False
507 self.
logger.info(f
'Error: {response.status_code}, {response.text}')
513 Get the JSON strings for the materials
514 @return: List of JSON strings for the materials. One string per material batch.
521 results.append(json.dumps(material, indent=4, sort_keys=
True))
526 Get list of material file names based on root file name.
527 @param rootName: The root name of the files to load. The files are assumed to be named: rootName_#.json
530 rootName = os.path.basename(rootName)
531 rootDir = os.path.dirname(rootName)
536 for root, dirs, files
in os.walk(rootDir):
540 if file.startswith(rootName)
and file.endswith(
'.json')
and file[len(rootName):-5].isdigit():
541 filePath = os.path.join(root, file)
542 filePaths.append(filePath)
547 @brief Read the material files from the "data/GPUOpenMaterialX" folder in the install Python package.
548 The files are expected to be named:
549 - "GPUOpenMaterialX_#.json" for material files,
550 - "GPUOpenMaterialX_Previews_.json" for material preview information, and
551 - "GPUOpenMaterialX_Names.json" for material names.
560 packageFolder = os.path.join(os.path.dirname(__file__),
'data/GPUOpenMaterialX')
561 for fileName
in os.listdir(packageFolder):
562 filePath = os.path.join(packageFolder, fileName)
563 self.
logger.debug(f
'> SCAN package file: "{filePath}"')
565 if re.match(
r'GPUOpenMaterialX_\d+\.json', fileName):
566 self.
logger.debug(f
'> Read package file: "{filePath}"')
567 with open(filePath)
as f:
569 results = data[
'results']
570 results_count = len(results)
572 elif fileName ==
'GPUOpenMaterialX_Previews_.json':
573 self.
logger.debug(f
'> Read package file: "{filePath}"')
574 with open(filePath)
as f:
582 elif fileName ==
'GPUOpenMaterialX_Renders_.json':
583 self.
logger.debug(f
'> Read package file: "{filePath}"')
584 with open(filePath)
as f:
592 self.
logger.debug(f
'Loaded {len(self.materials)} material files, '
593 f
'{len(self.materialPreviews)} material previews, and '
594 f
'{len(self.materialNames)} material names, '
595 f
'{len(self.renders)} render files from package.')
600 Load the materials from a set of JSON files downloaded from
601 the GPUOpen material database.
604 for fileName
in fileNames:
605 with open(fileName)
as f:
607 results = data[
'results']
608 results_count = len(results)
614 Write the render information to disk files.
615 @param folder: The folder to write the files to.
616 @param rootFileName: The root file name to use for the files.
617 @return: The number of files written.
622 os.makedirs(folder, exist_ok=
True)
624 fileName = rootFileName +
'_.json'
625 rendersFileName = os.path.join(folder, fileName)
626 self.
logger.info(f
'> Write render info to file: "{rendersFileName}"')
627 with open(rendersFileName,
'w')
as f:
628 json.dump(self.
renders, f, indent=4)
632 Write the material preview information to disk files.
633 @param folder: The folder to write the files to.
634 @param rootFileName: The root file name to use for the files.
639 os.makedirs(folder, exist_ok=
True)
641 fileName = rootFileName +
'_.json'
642 previewsFileName = os.path.join(folder, fileName)
643 self.
logger.info(f
'> Write material preview info to file: "{previewsFileName}"')
644 with open(previewsFileName,
'w')
as f:
650 Write the materials to disk.
651 @param folder: The folder to write the files to.
652 @param rootFileName: The root file name to use for the files.
653 @return: The number of files written.
660 os.makedirs(folder, exist_ok=
True)
663 fileName = rootFileName +
'_' + str(i) +
'.json'
664 materialFileName = os.path.join(folder, fileName)
665 self.
logger.info(f
'> Write material to file: "{materialFileName}"')
666 with open(materialFileName,
'w')
as f:
667 json.dump(material, f, indent=4, sort_keys=
True)
674 Write sorted list of the material names to a file in JSON format
675 @param fileName: The file name to write the material names to.
676 @param sort: If true, sort the material names.
681 with open(fileName,
'w')
as f:
This class is used to load materials from the GPUOpen material database.
writeMaterialNamesToFile(self, fileName, sort=True)
Write sorted list of the material names to a file in JSON format.
str render_url
URL for getting rendering information.
list getMaterialFileNames(self, rootName)
Get list of material file names based on root file name.
list getMaterials(self)
Get the materials returned from the GPUOpen material database.
dict renders
List of render information.
writeMaterialPreviewFile(self, folder, rootFileName)
Write the material preview information to disk files.
str getMaterialPreviewURL(self, title)
@breif Given the title of a material, return the URL for the material preview image.
int writeMaterialFiles(self, folder, rootFileName)
Write the materials to disk.
str package_url
URL for the package information.
convertPilImageToBase64(self, image)
Convert a PIL image to a Base64 string.
list materialPreviews
List of title, preview url pairs for the materials.
str root_url
Root URL for the GPUOpen material database.
downloadPackage(self, listNumber, materialNumber, packageId=0)
Download a package for a given material from the GPUOpen material database.
None readPackageFiles(self)
Read the material files from the "data/GPUOpenMaterialX" folder in the install Python package.
str url
URL for the materials.
extractPackageData(self, data, pilImage)
Extract the package data from a zip file.
bool writePackageDataToFile(self, data, outputFolder, title, url, unzipFile=True)
Write a package data to a file.
list getMaterialsAsJsonString(self)
Get the JSON strings for the materials.
int writeRenderFiles(self, folder, rootFileName)
Write the render information to disk files.
list computeMaterialPreviews(self)
Get the material preview URLs for the materials loaded from the GPUOpen material database.
list findMaterialsByName(self, materialName, exact_match=False)
Find materials by name.
list getRenders(self, force=False)
Get the rendering information returned from the GPUOpen material database.
int materials
List of materials.
list getMaterialNames(self)
Update the material names from the material lists.
list materialNames
List of material names.
downloadPackageByExpression(self, searchExpr, exact_match=False, packageId=0)
Download a package for a given material from the GPUOpen material database.
list readMaterialFiles(self, fileNames)
Load the materials from a set of JSON files downloaded from the GPUOpen material database.
__init__(self)
Initialize the GPUOpen material loader.
check_usdz_image_restrictions(self, zip_data, pilImage)
Check that the images in the zip file meet the USDZ format restrictions.