MaterialXMaterials 1.39.5
Utilities for retrieving materials from remote servers
Loading...
Searching...
No Matches
GPUOpenLoader.py
1'''
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.
4
5See: https://api.matlib.gpuopen.com/api/swagger/ for information on available API calls.
6'''
7
8import requests, json, os, io, re, zipfile, logging # type: ignore
9from http import HTTPStatus
10# Note: MaterialX is not currently a dependency since no MaterialX processing is required.
11#import MaterialX as mx
12
13import io
14import zipfile
15from io import BytesIO
16from PIL import Image as PILImage
17import base64
18
20 '''
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.
23 '''
24 def __init__(self):
25 '''
26 Initialize the GPUOpen material loader.
27 '''
28
29 self.root_url = 'https://api.matlib.gpuopen.com/api'
30
31 self.url = self.root_url + '/materials'
32
33 self.package_url = self.root_url + '/packages'
34
35 self.render_url = self.root_url + '/renders'
36
37 self.materialPreviews = None
38
39 self.materials = None
40
41 self.materialNames = None
42
43 self.renders = None
44
45
46 self.logger = logging.getLogger('GPUO')
47 logging.basicConfig(level=logging.INFO)
48
49 def writePackageDataToFile(self, data, outputFolder, title, url, unzipFile=True) -> bool:
50 '''
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
57 as the title.
58 @return: True if the package was written.
59 '''
60 if not data:
61 return False
62
63 if not os.path.exists(outputFolder):
64 os.makedirs(outputFolder)
65
66 if unzipFile:
67 # Assuming `data` is the binary data of the zip file and `title` and `outputFolder` are defined
68 unzipFolder = os.path.join(outputFolder, title)
69
70 # Use BytesIO to handle the data in memory
71 with io.BytesIO(data) as data_io:
72 with zipfile.ZipFile(data_io, 'r') as zip_ref:
73 zip_ref.extractall(unzipFolder)
74
75 # Write a url.txt file with url information for the material preview
76 urlFile = os.path.join(unzipFolder, 'url.txt')
77 with open(urlFile, 'w') as f:
78 f.write(url)
79
80 self.logger.info(f'Unzipped to folder: "{unzipFolder}"')
81
82 else:
83 # Add the url.txt into the existing zip data
84 zip_buffer = io.BytesIO()
85 # Open the original zip data in append mode
86 with zipfile.ZipFile(io.BytesIO(data), 'a', zipfile.ZIP_DEFLATED) as zip_in:
87 # Copy all files to a new zip buffer
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))
91 # Add url.txt
92 zip_out.writestr('url.txt', url)
93 data = zip_buffer.getvalue()
94
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}"')
98 f.write(data)
99
100
101 return True
102
103 def convertPilImageToBase64(self, image):
104 """
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
108 """
109 pilImage = PILImage
110 if not pilImage:
111 self.logger.debug('Pillow (PIL) image module not provided. Image data will not be converted to Base64.')
112 return None
113 if not image:
114 self.logger.debug('No image data. Image data will not be converted to Base64.')
115 return None
116
117 # - Create an in-memory buffer
118 # - Save the image to the buffer in PNG format
119 # - Get the PNG file data from the buffer
120 # - Encode the binary data to Base64
121 buffer = BytesIO()
122 image.save(buffer, format="PNG")
123 binary_data = buffer.getvalue()
124 base64_encoded_data = base64.b64encode(binary_data).decode('utf-8')
125 buffer.close()
126
127 return base64_encoded_data
128
129 def extractPackageData(self, data, pilImage):
130 '''
131 Extract the package data from a zip file.
132 @param data: The data to extract.
133 @param pilImage: The PIL image module.
134 @return: A list of extracted data of the form:
135 [ { 'file_name': file_name, 'data': data, 'type': type } ]
136 '''
137 if not pilImage:
138 pilImage = PILImage
139 if not pilImage:
140 self.logger.debug('Pillow (PIL) image module provided. Image data will not be extracted.')
141
142 zip_object = io.BytesIO(data)
143
144 extracted_data_list = []
145 with zipfile.ZipFile(zip_object, 'r') as zip_file:
146 # Iterate through the files in the zip archive
147 for file_name in zip_file.namelist():
148 # Extract each file into memory
149 extracted_data = zip_file.read(file_name)
150 if file_name.endswith('.mtlx'):
151 mtlx_string = extracted_data.decode('utf-8')
152 extracted_data_list.append( {'file_name': file_name, 'data': mtlx_string, 'type': 'mtlx'} )
153
154 # If the data is a image, create a image in Python
155 elif file_name.endswith('.png'):
156 if pilImage:
157 image = pilImage.open(io.BytesIO(extracted_data))
158 else:
159 image = None
160 extracted_data_list.append( {'file_name': file_name, 'data': image, 'type': 'image'} )
161
162 return extracted_data_list
163
164 def downloadPackage(self, listNumber, materialNumber, packageId=0):
165 '''
166 Download a package for a given material from the GPUOpen material database.
167 @param listNumber: The list number of the material to download.
168 @param materialNumber: The material number to download.
169 @param packageId: The package ID to download.
170 Packages are numbered starting at 0. Default is 0.
171 with index 0 containing the smallest package (smallest resolution referenced textures).
172 '''
173 if self.materials == None or len(self.materials) == 0:
174 self.logger.info('No material loaded.')
175 return [None, None]
176
177 json_data = self.materials[listNumber]
178 if not json_data:
179 self.logger.info(f'No material for list {listNumber}.')
180 return [None, None]
181
182 jsonResults = None
183 jsonResult = None
184 if "results" in json_data:
185 jsonResults = json_data["results"]
186 if len(jsonResults) <= materialNumber:
187 self.logger.info(f'No material for index {materialNumber}.')
188 return [None, None]
189 else:
190 jsonResult = jsonResults[materialNumber]
191
192 if not jsonResult:
193 return [None, None]
194
195 # Get the package ID
196 jsonPackages = None
197 if "packages" in jsonResult:
198 jsonPackages = jsonResult["packages"]
199 if not jsonPackages:
200 self.logger.info(f'No packages for material {materialNumber}.')
201 return [None, None]
202
203 if len(jsonPackages) <= packageId:
204 self.logger.info(f'No package for index {packageId}.')
205 return [None, None]
206 package_id = jsonPackages[packageId]
207
208 if not package_id:
209 self.logger.info(f'No package for index {packageId}.')
210 return [None, None]
211
212 url = f"{self.package_url}/{package_id}/download"
213 data = requests.get(url).content
214
215 title = jsonResult["title"]
216
217 preview_url = self.getMaterialPreviewURL(title)
218 return [data, title, preview_url]
219
220 def downloadPackageByExpression(self, searchExpr, exact_match=False, packageId=0):
221 '''
222 Download a package for a given material from the GPUOpen material database.
223 @param searchExpr: The regular expression to match the material name.
224 @param exact_match: If true, the material name must match exactly. Default is false.
225 @param packageId: The package ID to download.
226 @return: A list of downloaded packages of the form:
227 '''
228 downloadList = []
229
230 foundList = self.findMaterialsByName(searchExpr, exact_match)
231 if len(foundList) > 0:
232 for found in foundList:
233 listNumber = found['listNumber']
234 materialNumber = found['materialNumber']
235 matName = found['title']
236 self.logger.info(f'> Download material: {matName} List: {listNumber}. Index: {materialNumber}')
237 result = [data, title, url] = self.downloadPackage(listNumber, materialNumber, packageId)
238 downloadList.append(result)
239 return downloadList
240
241 def findMaterialsByName(self, materialName, exact_match = False) -> list:
242 '''
243 Find materials by name.
244 @param materialName: Regular expression to match the material name.
245 @param exact_match: If true, the material name must match exactly. Default is false.
246 @return: A list of materials that match the regular expression of the form:
247 [ { 'listNumber': listNumber, 'materialNumber': materialNumber, 'title': title } ]
248 '''
249 if (self.materials == None):
250 return []
251
252 materialsList = []
253 listNumber = 0
254 for materialList in self.materials:
255 materialNumber = 0
256 for material in materialList['results']:
257 if exact_match:
258 if materialName.lower() == material['title'].lower():
259 materialsList.append({ 'listNumber': listNumber, 'materialNumber': materialNumber, 'title': material['title'] })
260 return materialsList
261 else:
262 if re.match(materialName, material['title'], re.IGNORECASE):
263 materialsList.append({ 'listNumber': listNumber, 'materialNumber': materialNumber, 'title': material['title'] })
264 materialNumber += 1
265 listNumber += 1
266
267 return materialsList
268
269 def getMaterialNames(self) -> list:
270 '''
271 Update the material names from the material lists.
272 @return: List of material names. If no materials are loaded, then an empty list is returned.
273 '''
274 self.materialNames = []
275 if (self.materials == None):
276 return []
277
278 for materialList in self.materials:
279 for material in materialList['results']:
280 self.materialNames.append(material['title'])
281
282 return self.materialNames
283
284 def getMaterialPreviewURL(self, title) -> str:
285 '''
286 @breif Given the title of a material, return the URL for the material preview image.
287 @param title: The title of the material to get the preview URL for.
288 @return: The URL for the material preview image. Else empty string.
289 '''
290 url = ''
291 if (self.materialPreviews == None):
293 if (not self.materialPreviews):
294 return url
295
296 for item in self.materialPreviews:
297 if item['title'] == title:
298 url = item['preview_url']
299 break
300 return url
301
302 def getMaterialPreviews(self, force = False) -> list | None:
303 if not self.materialPreviews or force:
305 return self.materialPreviews
306
307 def computeMaterialPreviews(self) -> list:
308 '''
309 @brief Get the material preview URLs for the materials loaded from the GPUOpen material database.
310 @return list of items of the form: { 'title': material_title, 'preview_url': url }
311 If no materials or renders are loaded, then an empty list is returned.
312 '''
313 self.materialPreviews = []
314 if (self.materials == None):
315 return []
316 if (self.renders == None):
317 return []
318 render_urls = self.renders["renders"]
319
320 for materialList in self.materials:
321 for material in materialList['results']:
322 renders_order_list = material['renders_order']
323 material_title = material['title']
324
325 if len(renders_order_list) > 0:
326 #print('renders_order_list:', renders_order_list)
327 render_lookup = renders_order_list[0]
328 # Look for item in "renders" list with "id" == render_lookup
329 for render in render_urls:
330 #print('render id:', render['id'], 'render_lookup:', render_lookup )
331 if render['id'] == render_lookup:
332 url = render['thumbnail_url']
333 item = { 'title': material_title, 'preview_url': url }
334 self.materialPreviews.append(item)
335 #print(f'Found render for material: {item}')
336 break
337 else:
338 self.logger.info(f'No renderings specified for material: {material_title}')
339 return self.materialPreviews
340
341 def getMaterials(self) -> list:
342 '''
343 Get the materials returned from the GPUOpen material database.
344 Will loop based on the linked-list of materials stored in the database.
345 Currently the batch size requested is 100 materials per batch.
346 @return: List of material lists
347 '''
348
349 self.materials = []
350 self.materialNames = []
351
352 url = self.url
353 headers = {
354 'accept': 'application/json'
355 }
356
357 # Get batches of materials. Start with the first 100.
358 # Can apply more filters to this as needed in the future.
359 # This will get every material in the database.
360 params = {
361 'limit': 100,
362 'offset': 0
363 }
364 haveMoreMaterials = True
365 while (haveMoreMaterials):
366
367 response = requests.get(url, headers=headers, params=params)
368
369 if response.status_code == HTTPStatus.OK:
370
371 raw_response = response.text
372
373 # Split the response text assuming the JSON objects are concatenated
374 json_strings = raw_response.split('}{')
375 #self.logger.info('Number of JSON strings:', len(json_strings))
376 json_result_string = json_strings[0]
377 jsonObject = json.loads(json_result_string)
378 self.materials.append(jsonObject)
379
380 # Scan for next batch of materials
381 nextQuery = jsonObject['next']
382 if (nextQuery):
383 # Get limit and offset from this: 'https://api.matlib.gpuopen.com/api/materials/?limit=100&offset=100"'
384 # Split the string by '?'
385 queryParts = nextQuery.split('?')
386 # Split the string by '&'
387 queryParts = queryParts[1].split('&')
388 # Split the string by '='
389 limitParts = queryParts[0].split('=')
390 offsetParts = queryParts[1].split('=')
391 params['limit'] = int(limitParts[1])
392 params['offset'] = int(offsetParts[1])
393 self.logger.info(f'Fetch set of materials: limit: {params["limit"]} offset: {params["offset"]}')
394 else:
395 haveMoreMaterials = False
396 break
397
398 else:
399 self.logger.info(f'Error: {response.status_code}, {response.text}')
400
401 return self.materials
402
403 def getRenders(self, force=False) -> list:
404 '''
405 Get the rendering information returned from the GPUOpen material database.
406 Will loop based on the linked-list of render info stored in the database.
407 Currently the batch size requested is 100 render infos per batch.
408 @param force: If true, forces a re-download of the render information. Default is false.
409 @return: List of material lists
410 '''
411 if self.renders and not force:
412 return self.renders
413
414 self.renders = { "renders": [] }
415
416 url = self.render_url
417 headers = {
418 'accept': 'application/json'
419 }
420
421 # Get batches of materials. Start with the first 100.
422 # Can apply more filters to this as needed in the future.
423 # This will get every material in the database.
424 params = {
425 'limit': 100,
426 'offset': 0
427 }
428 haveMoreMaterials = True
429 while (haveMoreMaterials):
430
431 response = requests.get(url, headers=headers, params=params)
432
433 if response.status_code == HTTPStatus.OK:
434
435 raw_response = response.text
436
437 # Split the response text assuming the JSON objects are concatenated
438 json_strings = raw_response.split('}{')
439 #self.logger.info('Number of JSON strings:', len(json_strings))
440 json_result_string = json_strings[0]
441 jsonObject = json.loads(json_result_string)
442
443 # Extrac out the "results": [] list
444 results_list = jsonObject['results']
445 for result in results_list:
446 self.renders["renders"].append(result)
447
448 # Scan for next batch of materials
449 nextQuery = jsonObject['next']
450 if (nextQuery):
451 # Get limit and offset from this: 'https://api.matlib.gpuopen.com/api/renders/?limit=100&offset=100"'
452 # Split the string by '?'
453 queryParts = nextQuery.split('?')
454 # Split the string by '&'
455 queryParts = queryParts[1].split('&')
456 # Split the string by '='
457 limitParts = queryParts[0].split('=')
458 offsetParts = queryParts[1].split('=')
459 params['limit'] = int(limitParts[1])
460 params['offset'] = int(offsetParts[1])
461 self.logger.info(f'Fetch set of render infos: limit: {params["limit"]} offset: {params["offset"]}')
462 else:
463 haveMoreMaterials = False
464 break
465
466 else:
467 self.logger.info(f'Error: {response.status_code}, {response.text}')
468
469 return self.renders
470
471 def getMaterialsAsJsonString(self) -> list:
472 '''
473 Get the JSON strings for the materials
474 @return: List of JSON strings for the materials. One string per material batch.
475 '''
476 results : list = []
477
478 if (self.materials == None):
479 return results
480 for material in self.materials:
481 results.append(json.dumps(material, indent=4, sort_keys=True))
482 return results
483
484 def getMaterialFileNames(self, rootName) -> list:
485 '''
486 Get list of material file names based on root file name.
487 @param rootName: The root name of the files to load. The files are assumed to be named: rootName_#.json
488 '''
489 filePaths = []
490 rootName = os.path.basename(rootName)
491 rootDir = os.path.dirname(rootName)
492 if not rootDir:
493 rootDir = '.'
494 #print('RootDir:', rootDir)
495 #print('RootName:', rootName)
496 for root, dirs, files in os.walk(rootDir):
497 for file in files:
498 # Check that it ends with a number + ".json". e.g.
499 # "GPUOpenMaterialX_0.json"
500 if file.startswith(rootName) and file.endswith('.json') and file[len(rootName):-5].isdigit():
501 filePath = os.path.join(root, file)
502 filePaths.append(filePath)
503 return filePaths
504
505 def readPackageFiles(self) -> None:
506 '''
507 @brief Read the material files from the "data/GPUOpenMaterialX" folder in the install Python package.
508 The files are expected to be named:
509 - "GPUOpenMaterialX_#.json" for material files,
510 - "GPUOpenMaterialX_Previews_.json" for material preview information, and
511 - "GPUOpenMaterialX_Names.json" for material names.
512 '''
513
514 self.materials = []
515 self.materialPreviews = []
516 self.materialNames = []
517
518 # Read "data/GPUOpenMaterialX" files from install Python package
519 # Get package:
520 packageFolder = os.path.join(os.path.dirname(__file__), 'data/GPUOpenMaterialX')
521 for fileName in os.listdir(packageFolder):
522 filePath = os.path.join(packageFolder, fileName)
523 self.logger.debug(f'> SCAN package file: "{filePath}"')
524 # Check for files of this form: GPUOpenMaterialX_#.json
525 if re.match(r'GPUOpenMaterialX_\d+\.json', fileName):
526 self.logger.debug(f'> Read package file: "{filePath}"')
527 with open(filePath) as f:
528 data = json.load(f)
529 results = data['results']
530 results_count = len(results)
531 self.materials.append(data)
532 elif fileName == 'GPUOpenMaterialX_Previews_.json':
533 self.logger.debug(f'> Read package file: "{filePath}"')
534 with open(filePath) as f:
535 data = json.load(f)
536 self.materialPreviews = data
537 #elif fileName == 'GPUOpenMaterialX_Names.json':
538 # self.logger.debug(f'> Read package file: "{filePath}"')
539 # with open(filePath) as f:
540 # data = json.load(f)
541 # self.materialNames = data
542 elif fileName == 'GPUOpenMaterialX_Renders_.json':
543 self.logger.debug(f'> Read package file: "{filePath}"')
544 with open(filePath) as f:
545 data = json.load(f)
546 self.renders = data
547
548 # Better to extract the names from materials vs reading from file
549 # which may be out of sync.
550 self.getMaterialNames()
551
552 self.logger.debug(f'Loaded {len(self.materials)} material files, '
553 f'{len(self.materialPreviews)} material previews, and '
554 f'{len(self.materialNames)} material names, '
555 f'{len(self.renders)} render files from package.')
556
557
558 def readMaterialFiles(self, fileNames) -> list:
559 '''
560 Load the materials from a set of JSON files downloaded from
561 the GPUOpen material database.
562 '''
563 self.materials = []
564 for fileName in fileNames:
565 with open(fileName) as f:
566 data = json.load(f)
567 results = data['results']
568 results_count = len(results)
569 self.materials.append(data)
570 return self.materials
571
572 def writeRenderFiles(self, folder, rootFileName) -> int:
573 '''
574 Write the render information to disk files.
575 @param folder: The folder to write the files to.
576 @param rootFileName: The root file name to use for the files.
577 @return: The number of files written.
578 '''
579 if (self.renders == None):
580 return 0
581
582 os.makedirs(folder, exist_ok=True)
583 # Write JSON to file
584 fileName = rootFileName + '_.json'
585 rendersFileName = os.path.join(folder, fileName)
586 self.logger.info(f'> Write render info to file: "{rendersFileName}"')
587 with open(rendersFileName, 'w') as f:
588 json.dump(self.renders, f, indent=4)
589
590 def writeMaterialPreviewFile(self, folder, rootFileName):
591 '''
592 Write the material preview information to disk files.
593 @param folder: The folder to write the files to.
594 @param rootFileName: The root file name to use for the files.
595 '''
596 if (self.materialPreviews == None):
597 return 0
598
599 os.makedirs(folder, exist_ok=True)
600 # Write JSON to file
601 fileName = rootFileName + '_.json'
602 previewsFileName = os.path.join(folder, fileName)
603 self.logger.info(f'> Write material preview info to file: "{previewsFileName}"')
604 with open(previewsFileName, 'w') as f:
605 json.dump(self.materialPreviews, f, indent=4)
606
607
608 def writeMaterialFiles(self, folder, rootFileName) -> int:
609 '''
610 Write the materials to disk.
611 @param folder: The folder to write the files to.
612 @param rootFileName: The root file name to use for the files.
613 @return: The number of files written.
614 '''
615 if (self.materials == None):
616 return 0
617
618 i = 0
619 if (len(self.materials) > 0):
620 os.makedirs(folder, exist_ok=True)
621 for material in self.materials:
622 # Write JSON to file
623 fileName = rootFileName + '_' + str(i) + '.json'
624 materialFileName = os.path.join(folder, fileName)
625 self.logger.info(f'> Write material to file: "{materialFileName}"')
626 with open(materialFileName, 'w') as f:
627 json.dump(material, f, indent=4, sort_keys=True)
628 i += 1
629
630 return i
631
632 def writeMaterialNamesToFile(self, fileName, sort=True):
633 '''
634 Write sorted list of the material names to a file in JSON format
635 @param fileName: The file name to write the material names to.
636 @param sort: If true, sort the material names.
637 '''
638 if (self.materialNames == None):
639 return
640
641 with open(fileName, 'w') as f:
642 json.dump(self.materialNames, f, indent=2, sort_keys=sort)
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.
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.
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.
list getMaterialNames(self)
Update the material names from the material lists.
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.