Source code for usdmanager.parsers.usd

#
# Copyright 2020 DreamWorks Animation L.L.C.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""
USD file parsers
"""
import logging
import os
import re
from os.path import sep, splitext
from xml.sax.saxutils import escape, unescape

from Qt.QtCore import QFileInfo, Slot

from .. import utils
from ..constants import FILE_FORMAT_USD, FILE_FORMAT_USDA, FILE_FORMAT_USDC,\
    USD_AMBIGUOUS_EXTS, USD_ASCII_EXTS, USD_CRATE_EXTS
from ..parser import AbstractExtParser


# Set up logging.
logger = logging.getLogger(__name__)
logging.basicConfig()


NT = os.name == "nt"
NT_REGEX = re.compile(r'^[a-zA-Z]:[/\\]')


[docs]class UsdAsciiParser(AbstractExtParser): """ USD ASCII files. Treat as plain text. This is the simplest of the Usd parsers, which other USD parsers should inherit from. """ exts = USD_ASCII_EXTS fileFormat = FILE_FORMAT_USDA def __init__(self, *args, **kwargs): super(UsdAsciiParser, self).__init__(*args, **kwargs) self.regex = None self.usdArrayRegEx = re.compile( r"((?:\s*(?:\w+\s+)?\w+\[\]\s+[\w:]+\s*=|\s*\d+:)\s*\[)" # Array attribute definition and equal sign, or a frame number and colon, plus the opening bracket. r"\s*(.*)\s*" # Everything inside the square brackets. r"(\].*)$" # Closing bracket to the end of the line. )
[docs] @Slot() def compile(self): """ Compile regular expression to find links in USD files. """ self.regex = utils.usdRegEx(self.parent().programs.keys())
[docs] def parse(self, nativeAbsPath, fileInfo, link): """ Parse a file for links, generating a plain text version and HTML version of the file text. :Parameters: nativeAbsPath : `str` OS-native absolute file path fileInfo : `QFileInfo` File info object link : `QUrl` Full file path URL """ # Preserve any :SDF_FORMAT_ARGS: parameters from the current link. self.sdf_format_args = utils.sdfQuery(link) self.extractedDir = utils.queryItemValue(link, "extractedDir") return super(UsdAsciiParser, self).parse(nativeAbsPath, fileInfo, link)
[docs] def parseMatch(self, match, linkPath, nativeAbsPath, fileInfo): """ Parse a RegEx match of a path to another file. Override for specific language parsing. :Parameters: match RegEx match object linkPath : `str` Displayed file path matched by the RegEx nativeAbsPath : `str` OS-native absolute file path for the file being parsed fileInfo : `QFileInfo` File info object for the file being parsed :Returns: HTML link :Rtype: `str` :Raises ValueError: If path does not exist or cannot be resolved. """ # Since we had to escape all potential HTML-related characters before finding links, undo any replaced # by escape if part of the linkPath itself. URIs may have & as part of the path for query parameters. # We then have to re-escape the path before inserting it into HTML. linkPath = unescape(linkPath) expanded_path = utils.expandPath( linkPath, nativeAbsPath, self.sdf_format_args, extractedDir=self.extractedDir) if QFileInfo(linkPath).isAbsolute(): fullPath = QFileInfo(expanded_path).absoluteFilePath() logger.debug("Parsed link is absolute (%s). Expanded to %s", linkPath, fullPath) else: # Relative path from the current file to the link. fullPath = fileInfo.dir().absoluteFilePath(expanded_path) logger.debug("Parsed link is relative (%s). Expanded to %s", linkPath, fullPath) # Override any previously set sdf format args. local_sdf_args = self.sdf_format_args.copy() if match.group(3): for kv in match.group(3).split("&"): k, v = kv.split("=", 1) expanded_path = utils.expandPath( v, nativeAbsPath, self.sdf_format_args, extractedDir=self.extractedDir) local_sdf_args[k] = expanded_path.replace("&", "+").replace("=", ":") if local_sdf_args: queryParams = ["sdf=" + "+".join("{}:{}".format(k, v) for k, v in sorted(local_sdf_args.items(), key=lambda x: x[0]))] else: queryParams = [] # .usdz file references (e.g. @set.usdz[foo/bar.usd]@) if match.group(2): queryParams.append("layer=" + match.group(2)) # Propogate the extracted archive if this resolved file is in the same archive if self.extractedDir and fullPath.startswith(self.extractedDir + sep): queryParams.append("extractedDir=" + self.extractedDir) def pathForLink(path): """Need three slashes before drive letter on Windows; this prepends one, so with the usual two URL slashes we'll get the proper format.""" return '/' + path if NT and NT_REGEX.match(fullPath) else path # Make the HTML link. if self.exists[fullPath]: _, fullPathExt = splitext(fullPath) if fullPathExt[1:] in USD_CRATE_EXTS or (fullPathExt[1:] in USD_AMBIGUOUS_EXTS and utils.isUsdCrate(fullPath)): queryParams.insert(0, "binary=1") link = '<a class="binary" href="file://{}?{}">{}</a>'.format(pathForLink(fullPath), "&".join(queryParams), escape(linkPath)) logger.debug('parseMatch: created binary link <%s> for path <%s>', link, linkPath) queryStr = "?" + "&".join(queryParams) if queryParams else "" link = '<a href="file://{}{}">{}</a>'.format(pathForLink(fullPath), queryStr, escape(linkPath)) logger.debug('parseMatch: created link <%s> for path <%s>', link, linkPath) return link elif '*' in linkPath or '<UDIM>' in linkPath or '.#.' in linkPath: # Create an orange link for files with wildcards in the path, # designating zero or more files may exist. queryStr = "?" + "&".join(queryParams) if queryParams else "" return '<a title="Multiple files may exist" class="mayNotExist" href="file://{}{}">{}</a>'.format( pathForLink(fullPath), queryStr, escape(linkPath)) queryStr = "?" + "&".join(queryParams) if queryParams else "" return '<a title="File not found" class="badLink" href="file://{}{}">{}</a>'.format( pathForLink(fullPath), queryStr, escape(linkPath))
[docs] def parseLongLine(self, line): """ Process a long line. Link parsing is skipped, and long USD arrays are truncated in the middle. :Parameters: line : `str` Line of text :Returns: Line of text :Rtype: `str` """ match = self.usdArrayRegEx.match(line) if match: # Try to display just the first and last items in the long array with an ellipsis in the middle. # This drastically improves text browser interactivity and syntax highlighting time. logger.debug("Hiding long array") # Try to split to the first true item based on open parentheses. # This is hacky and prone to error if users have hand-edited the files. innerData = match.group(2) if innerData.startswith("(("): split = "))," elif innerData.startswith("("): split = ")," else: split = "," innerData = innerData.split(split, 1)[0] + split +\ "<span title='Long array truncated for display performance'> &hellip; </span>" +\ innerData.rsplit(split, 1)[-1].lstrip() return "{}{}{}\n".format(match.group(1), innerData, match.group(3)) return super(UsdAsciiParser, self).parseLongLine(line)
[docs]class UsdCrateParser(UsdAsciiParser): """ Parse USD file assuming it is a crate file. Don't bother checking the fist line for PXR-USDC. If this is a valid ASCII USD file and not binary, but we use this parser accidentally, the file will load slower (since we do a usdcat conversion) but won't break anything. """ exts = USD_CRATE_EXTS fileFormat = FILE_FORMAT_USDC
[docs] def acceptsFile(self, fileInfo, link): """ Accept .usdc files, or .usd files that do have a true binary query string value (i.e. .usd files we've already confirmed are crate). :Parameters: fileInfo : `QFileInfo` File info object link : `QtCore.QUrl` Full URL, potentially with query string """ ext = fileInfo.suffix() return ext in self.exts or (ext in USD_AMBIGUOUS_EXTS and utils.queryItemBoolValue(link, "binary"))
[docs] def read(self, path): return self.parent().readUsdCrateFile(path)
[docs]class UsdParser(UsdAsciiParser): """ Parse ambiguous USD files that may be ASCII or crate. """ exts = USD_AMBIGUOUS_EXTS fileFormat = FILE_FORMAT_USD
[docs] def acceptsFile(self, fileInfo, link): """ Accept .usd files that do not have a true binary query string in the URL (i.e. we haven't yet opened this file to determine if it is crate, or we have checked and it wasn't crate). :Parameters: fileInfo : `QFileInfo` File info object link : `QtCore.QUrl` Full URL, potentially with query string """ return fileInfo.suffix() in self.exts and not utils.queryItemBoolValue(link, "binary")
[docs] def read(self, path): with open(path) as f: # Read in the first line. If it's a binary USD file, # convert it to a temp ASCII file for viewing/editing. if f.readline().startswith("PXR-USDC"): self.fileFormat = FILE_FORMAT_USDC return self.parent().readUsdCrateFile(path) self.fileFormat = FILE_FORMAT_USDA # Read in the full file. f.seek(0) return f.readlines()