Package pyffi :: Package spells :: Package nif :: Module dump
[hide private]
[frames] | no frames]

Source Code for Module pyffi.spells.nif.dump

  1  """Spells for dumping particular blocks from nifs.""" 
  2   
  3  # -------------------------------------------------------------------------- 
  4  # ***** BEGIN LICENSE BLOCK ***** 
  5  # 
  6  # Copyright (c) 2007-2011, NIF File Format Library and Tools. 
  7  # All rights reserved. 
  8  # 
  9  # Redistribution and use in source and binary forms, with or without 
 10  # modification, are permitted provided that the following conditions 
 11  # are met: 
 12  # 
 13  #    * Redistributions of source code must retain the above copyright 
 14  #      notice, this list of conditions and the following disclaimer. 
 15  # 
 16  #    * Redistributions in binary form must reproduce the above 
 17  #      copyright notice, this list of conditions and the following 
 18  #      disclaimer in the documentation and/or other materials provided 
 19  #      with the distribution. 
 20  # 
 21  #    * Neither the name of the NIF File Format Library and Tools 
 22  #      project nor the names of its contributors may be used to endorse 
 23  #      or promote products derived from this software without specific 
 24  #      prior written permission. 
 25  # 
 26  # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 
 27  # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 
 28  # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 
 29  # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 
 30  # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 
 31  # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 
 32  # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 
 33  # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 
 34  # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 
 35  # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 
 36  # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
 37  # POSSIBILITY OF SUCH DAMAGE. 
 38  # 
 39  # ***** END LICENSE BLOCK ***** 
 40  # -------------------------------------------------------------------------- 
 41   
 42  import BaseHTTPServer 
 43  import ntpath # explicit windows style path manipulations 
 44  import os 
 45  import tempfile 
 46  import types 
 47  import webbrowser 
 48  from xml.sax.saxutils import escape # for htmlreport 
 49   
 50  from pyffi.formats.nif import NifFormat 
 51  from pyffi.spells.nif import NifSpell 
52 53 -def tohex(value, nbytes=4):
54 """Improved version of hex.""" 55 return ("0x%%0%dX" % (2*nbytes)) % (long(str(value)) & (2**(nbytes*8)-1))
56
57 -def dumpArray(arr):
58 """Format an array. 59 60 :param arr: An array. 61 :type arr: L{pyffi.object_models.xml.array.Array} 62 :return: String describing the array. 63 """ 64 text = "" 65 if arr._count2 == None: 66 for i, element in enumerate(list.__iter__(arr)): 67 if i > 16: 68 text += "etc...\n" 69 break 70 text += "%i: %s\n" % (i, dumpAttr(element)) 71 else: 72 k = 0 73 for i, elemlist in enumerate(list.__iter__(arr)): 74 for j, elem in enumerate(list.__iter__(elemlist)): 75 if k > 16: 76 text += "etc...\n" 77 break 78 text += "%i, %i: %s\n" % (i, j, dumpAttr(elem)) 79 k += 1 80 if k > 16: 81 break 82 return text if text else "None"
83
84 -def dumpBlock(block):
85 """Return formatted string for block without following references. 86 87 :param block: The block to print. 88 :type block: L{NifFormat.NiObject} 89 :return: String string describing the block. 90 """ 91 text = '%s instance at 0x%08X\n' % (block.__class__.__name__, id(block)) 92 for attr in block._get_filtered_attribute_list(): 93 attr_str_lines = \ 94 dumpAttr(getattr(block, "_%s_value_" % attr.name)).splitlines() 95 if len(attr_str_lines) > 1: 96 text += '* %s :\n' % attr.name 97 for attr_str in attr_str_lines: 98 text += ' %s\n' % attr_str 99 elif attr_str_lines: 100 text += '* %s : %s\n' % (attr.name, attr_str_lines[0]) 101 else: 102 text = '* %s : <None>\n' % attr.name 103 return text
104
105 -def dumpAttr(attr):
106 """Format an attribute. 107 108 :param attr: The attribute to print. 109 :type attr: (anything goes) 110 :return: String for the attribute. 111 """ 112 if isinstance(attr, (NifFormat.Ref, NifFormat.Ptr)): 113 ref = attr.get_value() 114 if ref: 115 if (hasattr(ref, "name")): 116 return "<%s:%s:0x%08X>" % (ref.__class__.__name__, 117 ref.name, id(attr)) 118 else: 119 return "<%s:0x%08X>" % (ref.__class__.__name__,id(attr)) 120 else: 121 return "<None>" 122 elif isinstance(attr, list): 123 return dumpArray(attr) 124 elif isinstance(attr, NifFormat.NiObject): 125 raise TypeError("cannot dump NiObject as attribute") 126 elif isinstance(attr, NifFormat.byte): 127 return tohex(attr.get_value(), 1) 128 elif isinstance(attr, (NifFormat.ushort, NifFormat.short)): 129 return tohex(attr.get_value(), 2) 130 elif isinstance(attr, (NifFormat.int, NifFormat.uint)): 131 return tohex(attr.get_value(), 4) 132 elif isinstance(attr, (types.IntType, types.LongType)): 133 return tohex(attr, 4) 134 else: 135 return str(attr)
136
137 -class SpellDumpAll(NifSpell):
138 """Dump the whole nif file.""" 139 140 SPELLNAME = "dump" 141
142 - def branchentry(self, branch):
143 # dump it 144 self.toaster.msg(dumpBlock(branch)) 145 # continue recursion 146 return True
147
148 -class SpellDumpTex(NifSpell):
149 """Dump the texture and material info of all geometries.""" 150 151 SPELLNAME = "dump_tex" 152
153 - def branchinspect(self, branch):
154 # stick to main tree nodes, and material and texture properties 155 return isinstance(branch, (NifFormat.NiAVObject, 156 NifFormat.NiTexturingProperty, 157 NifFormat.NiMaterialProperty))
158
159 - def branchentry(self, branch):
160 if isinstance(branch, NifFormat.NiTexturingProperty): 161 for textype in ('base', 'dark', 'detail', 'gloss', 'glow', 162 'bump_map', 'decal_0', 'decal_1', 'decal_2', 163 'decal_3'): 164 if getattr(branch, 'has_%s_texture' % textype): 165 texdesc = getattr(branch, 166 '%s_texture' % textype) 167 if texdesc.source: 168 if texdesc.source.use_external: 169 filename = texdesc.source.file_name 170 else: 171 filename = '(pixel data packed in file)' 172 else: 173 filename = '(no texture file)' 174 self.toaster.msg("[%s] %s" % (textype, filename)) 175 self.toaster.msg("apply mode %i" % branch.apply_mode) 176 # stop recursion 177 return False 178 elif isinstance(branch, NifFormat.NiMaterialProperty): 179 for coltype in ['ambient', 'diffuse', 'specular', 'emissive']: 180 col = getattr(branch, '%s_color' % coltype) 181 self.toaster.msg('%-10s %4.2f %4.2f %4.2f' 182 % (coltype, col.r, col.g, col.b)) 183 self.toaster.msg('glossiness %f' % branch.glossiness) 184 self.toaster.msg('alpha %f' % branch.alpha) 185 # stop recursion 186 return False 187 else: 188 # keep looking for blocks of interest 189 return True
190
191 -class SpellHtmlReport(NifSpell):
192 """Make a html report of selected blocks.""" 193 194 SPELLNAME = "dump_htmlreport" 195 ENTITIES = { "\n": "<br/>" } 196 197 @classmethod
198 - def toastentry(cls, toaster):
199 # maps each block type to a list of reports for that block type 200 toaster.reports_per_blocktype = {} 201 # spell always applies 202 return True
203
204 - def _branchinspect(self, branch):
205 # enter every branch 206 # (the base method is called in branch entry) 207 return True
208
209 - def branchentry(self, branch):
210 # check if this branch must be checked, if not, recurse further 211 if not NifSpell._branchinspect(self, branch): 212 return True 213 blocktype = branch.__class__.__name__ 214 reports = self.toaster.reports_per_blocktype.get(blocktype) 215 if not reports: 216 # start a new report for this block type 217 row = "<tr>" 218 row += "<th>%s</th>" % "file" 219 row += "<th>%s</th>" % "id" 220 for attr in branch._get_filtered_attribute_list(data=self.data): 221 row += ("<th>%s</th>" 222 % escape(attr.displayname, self.ENTITIES)) 223 row += "</tr>" 224 reports = [row] 225 self.toaster.reports_per_blocktype[blocktype] = reports 226 227 row = "<tr>" 228 row += "<td>%s</td>" % escape(self.stream.name) 229 row += "<td>%s</td>" % escape("0x%08X" % id(branch), self.ENTITIES) 230 for attr in branch._get_filtered_attribute_list(data=self.data): 231 row += ("<td>%s</td>" 232 % escape(dumpAttr(getattr(branch, "_%s_value_" 233 % attr.name)), 234 self.ENTITIES)) 235 row += "</tr>" 236 reports.append(row) 237 # keep looking for blocks of interest 238 return True
239 240 @classmethod
241 - def toastexit(cls, toaster):
242 if toaster.reports_per_blocktype: 243 rows = [] 244 rows.append( "<head>" ) 245 rows.append( "<title>Report</title>" ) 246 rows.append( "</head>" ) 247 rows.append( "<body>" ) 248 249 for blocktype, reports in toaster.reports_per_blocktype.iteritems(): 250 rows.append("<h1>%s</h1>" % blocktype) 251 rows.append('<table border="1" cellspacing="0">') 252 rows.append("\n".join(reports)) 253 rows.append("</table>") 254 255 rows.append("</body>") 256 257 cls.browser("\n".join(rows)) 258 else: 259 toaster.msg('No Report Generated')
260 261 @classmethod
262 - def browser(cls, htmlstr):
263 """Display html in the default web browser without creating a 264 temp file. 265 266 Instantiates a trivial http server and calls webbrowser.open 267 with a URL to retrieve html from that server. 268 """ 269 class RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): 270 def do_GET(self): 271 bufferSize = 1024*1024 272 for i in xrange(0, len(htmlstr), bufferSize): 273 self.wfile.write(htmlstr[i:i+bufferSize])
274 275 server = BaseHTTPServer.HTTPServer(('127.0.0.1', 0), RequestHandler) 276 webbrowser.open('http://127.0.0.1:%s' % server.server_port) 277 server.handle_request() 278
279 -class SpellExportPixelData(NifSpell):
280 """Export embedded images as DDS files. If the toaster's 281 ``--dryrun`` option is enabled, the image is written to a 282 temporary file, otherwise, if no further path information is 283 stored in the nif, it is written to 284 ``<nifname>-pixeldata-<n>.dds``. If a path is stored in the nif, 285 then the original file path is used. 286 287 The ``--arg`` option is used to strip the folder part of the path 288 and to replace it with something else (this is sometimes useful, 289 such as in Bully nft files). 290 291 The file extension is forced to ``.dds``. 292 """ 293 294 SPELLNAME = "dump_pixeldata" 295
296 - def __init__(self, *args, **kwargs):
297 NifSpell.__init__(self, *args, **kwargs) 298 self.pixeldata_counter = 0 299 """Increments on each pixel data block."""
300
301 - def datainspect(self):
303
304 - def branchinspect(self, branch):
305 # stick to main tree nodes, and material and texture properties 306 return isinstance(branch, (NifFormat.NiAVObject, 307 NifFormat.NiTexturingProperty, 308 NifFormat.NiSourceTexture, 309 NifFormat.ATextureRenderData))
310
311 - def branchentry(self, branch):
312 313 if (isinstance(branch, NifFormat.NiSourceTexture) 314 and branch.pixel_data and branch.file_name): 315 self.save_as_dds(branch.pixel_data, branch.file_name) 316 return False 317 elif isinstance(branch, NifFormat.ATextureRenderData): 318 filename = "%s-pixeldata-%i" % ( 319 os.path.basename(self.stream.name), 320 self.pixeldata_counter) 321 self.save_as_dds(branch, filename) 322 self.pixeldata_counter += 1 323 return False 324 else: 325 # keep recursing 326 return True
327 328 @classmethod
329 - def get_toast_stream(cls, toaster, filename, test_exists=False):
330 """We do not toast the original file, so stream construction 331 is delegated to :meth:`get_toast_pixeldata_stream`. 332 """ 333 if test_exists: 334 return False 335 else: 336 return None
337 338 @staticmethod
339 - def get_pixeldata_head_root(texture_filename):
340 r"""Transform NiSourceTexture.file_name into something workable. 341 342 >>> SpellExportPixelData.get_pixeldata_head_root("test.tga") 343 ('', 'test') 344 >>> SpellExportPixelData.get_pixeldata_head_root(r"textures\test.tga") 345 ('textures', 'test') 346 >>> SpellExportPixelData.get_pixeldata_head_root( 347 ... r"Z:\Bully\Temp\Export\Textures\Clothing\P_Pants1\P_Pants1_d.tga") 348 ('z:/bully/temp/export/textures/clothing/p_pants1', 'p_pants1_d') 349 """ 350 # note: have to use ntpath here so we can split correctly 351 # nif convention always uses windows style paths 352 head, tail = ntpath.split(texture_filename) 353 root, ext = ntpath.splitext(tail) 354 # for linux: make paths case insensitive by converting to lower case 355 head = head.lower() 356 root = root.lower() 357 # XXX following is disabled because not all textures in Bully 358 # XXX actually have this form; use "-a textures" for this game 359 # make relative path for Bully SE 360 #tmp1, tmp2, tmp3 = head.partition("\\bully\\temp\\export\\") 361 #if tmp2: 362 # head = tmp3 363 # for linux: convert backslash to forward slash 364 head = head.replace("\\", "/") 365 return (head, root) if root else ("", "image")
366
367 - def get_toast_pixeldata_stream(self, texture_filename):
368 # dry run: no file name 369 if self.toaster.options["dryrun"]: 370 self.toaster.msg("saving as temporary file") 371 return tempfile.TemporaryFile() 372 # get head and root, and override head if requested 373 head, root = self.get_pixeldata_head_root(texture_filename) 374 if self.toaster.options["arg"]: 375 head = self.toaster.options["arg"] 376 # create path to file of the to be exported pixeldata 377 toast_head = self.toaster.get_toast_head_root_ext(self.stream.name)[0] 378 head = os.path.join(toast_head, head) 379 if head: 380 if not os.path.exists(head): 381 self.toaster.msg("creating path %s" % head) 382 os.makedirs(head) 383 # create file 384 filename = os.path.join(head, root) + ".dds" 385 if self.toaster.options["resume"]: 386 if os.path.exists(filename): 387 self.toaster.msg("%s (already done)" % filename) 388 return None 389 self.toaster.msg("saving as %s" % filename) 390 return open(filename, "wb")
391
392 - def save_as_dds(self, pixeldata, texture_filename):
393 """Save pixeldata as dds file, using the specified filename.""" 394 self.toaster.msg("found pixel data (format %i)" 395 % pixeldata.pixel_format) 396 try: 397 stream = self.get_toast_pixeldata_stream(texture_filename) 398 if stream: 399 pixeldata.save_as_dds(stream) 400 finally: 401 if stream: 402 stream.close()
403