MaterialXMaterials 0.0.1
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
36
37 self.materialNames = None
38
39
40 self.logger = logging.getLogger('GPUO')
41 logging.basicConfig(level=logging.INFO)
42
43 def writePackageDataToFile(self, data, outputFolder, title, unzipFile=True) -> bool:
44 '''
45 Write a package data to a file.
46 @param data: The data to write.
47 @param outputFolder: The output folder to write the file to.
48 @param title: The title of the file.
49 @param unzipFile: If true, the file is unzipped to a folder with the same name
50 as the title.
51 @return: True if the package was written.
52 '''
53 if not data:
54 return False
55
56 if not os.path.exists(outputFolder):
57 os.makedirs(outputFolder)
58
59 if unzipFile:
60 # Assuming `data` is the binary data of the zip file and `title` and `outputFolder` are defined
61 unzipFolder = os.path.join(outputFolder, title)
62
63 # Use BytesIO to handle the data in memory
64 with io.BytesIO(data) as data_io:
65 with zipfile.ZipFile(data_io, 'r') as zip_ref:
66 zip_ref.extractall(unzipFolder)
67
68 self.logger.info(f'Unzipped to folder: "{unzipFolder}"')
69
70 else:
71 outputFile = os.path.join(outputFolder, f"{title}.zip")
72 with open(outputFile, "wb") as f:
73 self.logger.info(f'Write package to file: "{outputFile}"')
74 f.write(data)
75
76 return True
77
78 def convertPilImageToBase64(self, image):
79 """
80 Convert a PIL image to a Base64 string.
81 @param image: An instance of PIL.Image
82 @return: Base64-encoded string of the image
83 """
84 pilImage = PILImage
85 if not pilImage:
86 self.logger.debug('Pillow (PIL) image module not provided. Image data will not be converted to Base64.')
87 return None
88 if not image:
89 self.logger.debug('No image data. Image data will not be converted to Base64.')
90 return None
91
92 # - Create an in-memory buffer
93 # - Save the image to the buffer in PNG format
94 # - Get the PNG file data from the buffer
95 # - Encode the binary data to Base64
96 buffer = BytesIO()
97 image.save(buffer, format="PNG")
98 binary_data = buffer.getvalue()
99 base64_encoded_data = base64.b64encode(binary_data).decode('utf-8')
100 buffer.close()
101
102 return base64_encoded_data
103
104 def extractPackageData(self, data, pilImage):
105 '''
106 Extract the package data from a zip file.
107 @param data: The data to extract.
108 @param pilImage: The PIL image module.
109 @return: A list of extracted data of the form:
110 [ { 'file_name': file_name, 'data': data, 'type': type } ]
111 '''
112 if not pilImage:
113 pilImage = PILImage
114 if not pilImage:
115 self.logger.debug('Pillow (PIL) image module provided. Image data will not be extracted.')
116
117 zip_object = io.BytesIO(data)
118
119 extracted_data_list = []
120 with zipfile.ZipFile(zip_object, 'r') as zip_file:
121 # Iterate through the files in the zip archive
122 for file_name in zip_file.namelist():
123 # Extract each file into memory
124 extracted_data = zip_file.read(file_name)
125 if file_name.endswith('.mtlx'):
126 mtlx_string = extracted_data.decode('utf-8')
127 extracted_data_list.append( {'file_name': file_name, 'data': mtlx_string, 'type': 'mtlx'} )
128
129 # If the data is a image, create a image in Python
130 elif file_name.endswith('.png'):
131 if pilImage:
132 image = pilImage.open(io.BytesIO(extracted_data))
133 else:
134 image = None
135 extracted_data_list.append( {'file_name': file_name, 'data': image, 'type': 'image'} )
136
137 return extracted_data_list
138
139 def downloadPackage(self, listNumber, materialNumber, packageId=0):
140 '''
141 Download a package for a given material from the GPUOpen material database.
142 @param listNumber: The list number of the material to download.
143 @param materialNumber: The material number to download.
144 @param packageId: The package ID to download.
145 Packages are numbered starting at 0. Default is 0.
146 with index 0 containing the smallest package (smallest resolution referenced textures).
147 '''
148 if self.materialsmaterialsmaterials == None or len(self.materialsmaterialsmaterials) == 0:
149 self.logger.info('No material loaded.')
150 return [None, None]
151
152 json_data = self.materialsmaterialsmaterials[listNumber]
153 if not json_data:
154 self.logger.info(f'No material for list {listNumber}.')
155 return [None, None]
156
157 jsonResults = None
158 jsonResult = None
159 if "results" in json_data:
160 jsonResults = json_data["results"]
161 if len(jsonResults) <= materialNumber:
162 self.logger.info(f'No material for index {materialNumber}.')
163 return [None, None]
164 else:
165 jsonResult = jsonResults[materialNumber]
166
167 if not jsonResult:
168 return [None, None]
169
170 # Get the package ID
171 jsonPackages = None
172 if "packages" in jsonResult:
173 jsonPackages = jsonResult["packages"]
174 if not jsonPackages:
175 self.logger.info(f'No packages for material {materialNumber}.')
176 return [None, None]
177
178 if len(jsonPackages) <= packageId:
179 self.logger.info(f'No package for index {packageId}.')
180 return [None, None]
181 package_id = jsonPackages[packageId]
182
183 if not package_id:
184 self.logger.info(f'No package for index {packageId}.')
185 return [None, None]
186
187 url = f"{self.package_url}/{package_id}/download"
188 data = requests.get(url).content
189
190 title = jsonResult["title"]
191 return [data, title]
192
193 def downloadPackageByExpression(self, searchExpr, packageId=0):
194 '''
195 Download a package for a given material from the GPUOpen material database.
196 @param searchExpr: The regular expression to match the material name.
197 @param packageId: The package ID to download.
198 @return: A list of downloaded packages of the form:
199 '''
200 downloadList = []
201
202 foundList = self.findMaterialsByName(searchExpr)
203 if len(foundList) > 0:
204 for found in foundList:
205 listNumber = found['listNumber']
206 materialNumber = found['materialNumber']
207 matName = found['title']
208 self.logger.info(f'> Download material: {matName} List: {listNumber}. Index: {materialNumber}')
209 result = [data, title] = self.downloadPackage(listNumber, materialNumber, packageId)
210 downloadList.append(result)
211 return downloadList
212
213 def findMaterialsByName(self, materialName) -> list:
214 '''
215 Find materials by name.
216 @param materialName: Regular expression to match the material name.
217 @return: A list of materials that match the regular expression of the form:
218 [ { 'listNumber': listNumber, 'materialNumber': materialNumber, 'title': title } ]
219 '''
220 if (self.materialsmaterialsmaterials == None):
221 return []
222
223 materialsList = []
224 listNumber = 0
225 for materialList in self.materialsmaterialsmaterials:
226 materialNumber = 0
227 for material in materialList['results']:
228 if re.match(materialName, material['title'], re.IGNORECASE):
229 materialsList.append({ 'listNumber': listNumber, 'materialNumber': materialNumber, 'title': material['title'] })
230 materialNumber += 1
231 listNumber += 1
232
233 return materialsList
234
235 def getMaterialNames(self) -> list:
236 '''
237 Update the material names from the material lists.
238 @return: List of material names. If no materials are loaded, then an empty list is returned.
239 '''
240 self.materialNames = []
241 if (self.materialsmaterialsmaterials == None):
242 return []
243
244 for materialList in self.materialsmaterialsmaterials:
245 for material in materialList['results']:
246 self.materialNames.append(material['title'])
247
248 return self.materialNames
249
250 def getMaterials(self) -> list:
251 '''
252 Get the materials returned from the GPUOpen material database.
253 Will loop based on the linked-list of materials stored in the database.
254 Currently the batch size requested is 100 materials per batch.
255 @return: List of material lists
256 '''
257
259 self.materialNames = []
260
261 url = self.url
262 headers = {
263 'accept': 'application/json'
264 }
265
266 # Get batches of materials. Start with the first 100.
267 # Can apply more filters to this as needed in the future.
268 # This will get every material in the database.
269 params = {
270 'limit': 100,
271 'offset': 0
272 }
273 haveMoreMaterials = True
274 while (haveMoreMaterials):
275
276 response = requests.get(url, headers=headers, params=params)
277
278 if response.status_code == HTTPStatus.OK:
279
280 raw_response = response.text
281
282 # Split the response text assuming the JSON objects are concatenated
283 json_strings = raw_response.split('}{')
284 #self.logger.info('Number of JSON strings:', len(json_strings))
285 json_result_string = json_strings[0]
286 jsonObject = json.loads(json_result_string)
287 self.materialsmaterialsmaterials.append(jsonObject)
288
289 # Scan for next batch of materials
290 nextQuery = jsonObject['next']
291 if (nextQuery):
292 # Get limit and offset from this: 'https://api.matlib.gpuopen.com/api/materials/?limit=100&offset=100"'
293 # Split the string by '?'
294 queryParts = nextQuery.split('?')
295 # Split the string by '&'
296 queryParts = queryParts[1].split('&')
297 # Split the string by '='
298 limitParts = queryParts[0].split('=')
299 offsetParts = queryParts[1].split('=')
300 params['limit'] = int(limitParts[1])
301 params['offset'] = int(offsetParts[1])
302 self.logger.info(f'Fetch set of materials: limit: {params["limit"]} offset: {params["offset"]}')
303 else:
304 haveMoreMaterials = False
305 break
306
307 else:
308 self.logger.info(f'Error: {response.status_code}, {response.text}')
309
310 return self.materialsmaterialsmaterials
311
312 def getMaterialsAsJsonString(self) -> list:
313 '''
314 Get the JSON strings for the materials
315 @return: List of JSON strings for the materials. One string per material batch.
316 '''
317 results : list = []
318
319 if (self.materialsmaterialsmaterials == None):
320 return results
321 for material in self.materialsmaterialsmaterials:
322 results.append(json.dumps(material, indent=4, sort_keys=True))
323 return results
324
325 def getMaterialFileNames(self, rootName) -> list:
326 '''
327 Get list of material file names based on root file name.
328 @param rootName: The root name of the files to load. The files are assumed to be named: rootName_#.json
329 '''
330 filePaths = []
331 rootName = os.path.basename(rootName)
332 rootDir = os.path.dirname(rootName)
333 if not rootDir:
334 rootDir = '.'
335 print('RootDir:', rootDir)
336 print('RootName:', rootName)
337 for root, dirs, files in os.walk(rootDir):
338 for file in files:
339 # Check that it ends with a number + ".json". e.g.
340 # "GPUOpenMaterialX_0.json"
341 if file.startswith(rootName) and file.endswith('.json') and file[len(rootName):-5].isdigit():
342 filePath = os.path.join(root, file)
343 filePaths.append(filePath)
344 return filePaths
345
346 def readMaterialFiles(self, fileNames) -> list:
347 '''
348 Load the materials from a set of JSON files downloaded from
349 the GPUOpen material database.
350 '''
352 for fileName in fileNames:
353 with open(fileName) as f:
354 data = json.load(f)
355 results = data['results']
356 results_count = len(results)
357 self.materialsmaterialsmaterials.append(data)
359
360 def writeMaterialFiles(self, folder, rootFileName) -> int:
361 '''
362 Write the materials to a set of MaterialX files.
363 @param folder: The folder to write the files to.
364 @param rootFileName: The root file name to use for the files.
365 @return: The number of files written.
366 '''
367 if (self.materialsmaterialsmaterials == None):
368 return 0
369
370 i = 0
371 if (len(self.materialsmaterialsmaterials) > 0):
372 os.makedirs(folder, exist_ok=True)
373 for material in self.materialsmaterialsmaterials:
374 # Write JSON to file
375 fileName = rootFileName + '_' + str(i) + '.json'
376 materialFileName = os.path.join(folder, fileName)
377 self.logger.info(f'> Write material to file: "{materialFileName}"')
378 with open(materialFileName, 'w') as f:
379 json.dump(material, f, indent=4, sort_keys=True)
380 i += 1
381
382 return i
383
384 def writeMaterialNamesToFile(self, fileName, sort=True):
385 '''
386 Write sorted list of the material names to a file in JSON format
387 @param fileName: The file name to write the material names to.
388 @param sort: If true, sort the material names.
389 '''
390 if (self.materialNames == None):
391 return
392
393 with open(fileName, 'w') as f:
394 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.
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.
int writeMaterialFiles(self, folder, rootFileName)
Write the materials to a set of MaterialX files.
convertPilImageToBase64(self, image)
Convert a PIL image to a Base64 string.
bool writePackageDataToFile(self, data, outputFolder, title, unzipFile=True)
Write a package data to a file.
downloadPackageByExpression(self, searchExpr, packageId=0)
Download a package for a given material from the GPUOpen material database.
str root_url
Root URL for the GPUOpen material database.
list findMaterialsByName(self, materialName)
Find materials by name.
downloadPackage(self, listNumber, materialNumber, packageId=0)
Download a package for a given material from the GPUOpen material database.
extractPackageData(self, data, pilImage)
Extract the package data from a zip file.
list getMaterialsAsJsonString(self)
Get the JSON strings for the materials.
list getMaterialNames(self)
Update the material names from the material lists.
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.