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 return [None, None]
150
151 json_data = self.materialsmaterialsmaterials[listNumber]
152 if not json_data:
153 return [None, None]
154
155 jsonResults = None
156 jsonResult = None
157 if "results" in json_data:
158 jsonResults = json_data["results"]
159 if len(jsonResults) <= materialNumber:
160 return [None, None]
161 else:
162 jsonResult = jsonResults[materialNumber]
163
164 if not jsonResult:
165 return [None, None]
166
167 # Get the package ID
168 jsonPackages = None
169 if "packages" in jsonResult:
170 jsonPackages = jsonResult["packages"]
171 if not jsonPackages:
172 return [None, None]
173
174 if len(jsonPackages) <= packageId:
175 return [None, None]
176 package_id = jsonPackages[packageId]
177
178 if not package_id:
179 return [None, None]
180
181 url = f"{self.package_url}/{package_id}/download"
182 data = requests.get(url).content
183
184 title = jsonResult["title"]
185 return [data, title]
186
187 def downloadPackageByExpression(self, searchExpr, packageId=0):
188 '''
189 Download a package for a given material from the GPUOpen material database.
190 @param searchExpr: The regular expression to match the material name.
191 @param packageId: The package ID to download.
192 @return: A list of downloaded packages of the form:
193 '''
194 downloadList = []
195
196 foundList = self.findMaterialsByName(searchExpr)
197 if len(foundList) > 0:
198 for found in foundList:
199 listNumber = found['listNumber']
200 materialNumber = found['materialNumber']
201 matName = found['title']
202 self.logger.info(f'> Download material: {matName} List: {listNumber}. Index: {materialNumber}')
203 result = [data, title] = self.downloadPackage(listNumber, materialNumber, packageId)
204 downloadList.append(result)
205 return downloadList
206
207 def findMaterialsByName(self, materialName) -> list:
208 '''
209 Find materials by name.
210 @param materialName: Regular expression to match the material name.
211 @return: A list of materials that match the regular expression of the form:
212 [ { 'listNumber': listNumber, 'materialNumber': materialNumber, 'title': title } ]
213 '''
214 if (self.materialsmaterialsmaterials == None):
215 return []
216
217 materialsList = []
218 listNumber = 0
219 materialNumber = 0
220 for materialList in self.materialsmaterialsmaterials:
221 for material in materialList['results']:
222 if re.match(materialName, material['title'], re.IGNORECASE):
223 materialsList.append({ 'listNumber': listNumber, 'materialNumber': materialNumber, 'title': material['title'] })
224 materialNumber += 1
225 listNumber += 1
226
227 return materialsList
228
229 def getMaterialNames(self) -> list:
230 '''
231 Update the material names from the material lists.
232 @return: List of material names. If no materials are loaded, then an empty list is returned.
233 '''
234 self.materialNames = []
235 if (self.materialsmaterialsmaterials == None):
236 return []
237
238 for materialList in self.materialsmaterialsmaterials:
239 for material in materialList['results']:
240 self.materialNames.append(material['title'])
241
242 return self.materialNames
243
244 def getMaterials(self) -> list:
245 '''
246 Get the materials returned from the GPUOpen material database.
247 Will loop based on the linked-list of materials stored in the database.
248 Currently the batch size requested is 100 materials per batch.
249 @return: List of material lists
250 '''
251
253 self.materialNames = []
254
255 url = self.url
256 headers = {
257 'accept': 'application/json'
258 }
259
260 # Get batches of materials. Start with the first 100.
261 # Can apply more filters to this as needed in the future.
262 # This will get every material in the database.
263 params = {
264 'limit': 100,
265 'offset': 0
266 }
267 haveMoreMaterials = True
268 while (haveMoreMaterials):
269
270 response = requests.get(url, headers=headers, params=params)
271
272 if response.status_code == HTTPStatus.OK:
273
274 raw_response = response.text
275
276 # Split the response text assuming the JSON objects are concatenated
277 json_strings = raw_response.split('}{')
278 #self.logger.info('Number of JSON strings:', len(json_strings))
279 json_result_string = json_strings[0]
280 jsonObject = json.loads(json_result_string)
281 self.materialsmaterialsmaterials.append(jsonObject)
282
283 # Scan for next batch of materials
284 nextQuery = jsonObject['next']
285 if (nextQuery):
286 # Get limit and offset from this: 'https://api.matlib.gpuopen.com/api/materials/?limit=100&offset=100"'
287 # Split the string by '?'
288 queryParts = nextQuery.split('?')
289 # Split the string by '&'
290 queryParts = queryParts[1].split('&')
291 # Split the string by '='
292 limitParts = queryParts[0].split('=')
293 offsetParts = queryParts[1].split('=')
294 params['limit'] = int(limitParts[1])
295 params['offset'] = int(offsetParts[1])
296 self.logger.info(f'Fetch set of materials: limit: {params["limit"]} offset: {params["offset"]}')
297 else:
298 haveMoreMaterials = False
299 break
300
301 else:
302 self.logger.info(f'Error: {response.status_code}, {response.text}')
303
304 return self.materialsmaterialsmaterials
305
306 def getMaterialsAsJsonString(self) -> list:
307 '''
308 Get the JSON strings for the materials
309 @return: List of JSON strings for the materials. One string per material batch.
310 '''
311 results : list = []
312
313 if (self.materialsmaterialsmaterials == None):
314 return results
315 for material in self.materialsmaterialsmaterials:
316 results.append(json.dumps(material, indent=4, sort_keys=True))
317 return results
318
319 def readMaterialFiles(self, fileNames) -> list:
320 '''
321 Load the materials from a set of JSON files downloaded from
322 the GPUOpen material database.
323 '''
325 for fileName in fileNames:
326 with open(fileName) as f:
327 data = json.load(f)
328 self.materialsmaterialsmaterials.append(data)
330
331 def writeMaterialFiles(self, folder, rootFileName) -> int:
332 '''
333 Write the materials to a set of MaterialX files.
334 @param folder: The folder to write the files to.
335 @param rootFileName: The root file name to use for the files.
336 @return: The number of files written.
337 '''
338 if (self.materialsmaterialsmaterials == None):
339 return 0
340
341 i = 0
342 if (len(self.materialsmaterialsmaterials) > 0):
343 os.makedirs(folder, exist_ok=True)
344 for material in self.materialsmaterialsmaterials:
345 # Write JSON to file
346 fileName = rootFileName + '_' + str(i) + '.json'
347 materialFileName = os.path.join(folder, fileName)
348 self.logger.info(f'> Write material to file: "{materialFileName}"')
349 with open(materialFileName, 'w') as f:
350 json.dump(material, f, indent=4, sort_keys=True)
351 i += 1
352
353 return i
354
355 def writeMaterialNamesToFile(self, fileName, sort=True):
356 '''
357 Write sorted list of the material names to a file in JSON format
358 @param fileName: The file name to write the material names to.
359 @param sort: If true, sort the material names.
360 '''
361 if (self.materialNames == None):
362 return
363
364 with open(fileName, 'w') as f:
365 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 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.