1#
2# Copyright 2018 Pixar
3#
4# Licensed under the Apache License, Version 2.0 (the "Apache License")
5# with the following modification; you may not use this file except in
6# compliance with the Apache License and the following modification to it:
7# Section 6. Trademarks. is deleted and replaced with:
8#
9# 6. Trademarks. This License does not grant permission to use the trade
10#    names, trademarks, service marks, or product names of the Licensor
11#    and its affiliates, except as required to comply with Section 4(c) of
12#    the License and to reproduce the content of the NOTICE file.
13#
14# You may obtain a copy of the Apache License at
15#
16#     http://www.apache.org/licenses/LICENSE-2.0
17#
18# Unless required by applicable law or agreed to in writing, software
19# distributed under the Apache License with the above modification is
20# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
21# KIND, either express or implied. See the Apache License for the specific
22# language governing permissions and limitations under the Apache License.
23#
24
25from __future__ import print_function
26
27from pxr import Ar
28
29from pxr.UsdUtils.constantsGroup import ConstantsGroup
30
31class NodeTypes(ConstantsGroup):
32    UsdPreviewSurface = "UsdPreviewSurface"
33    UsdUVTexture = "UsdUVTexture"
34    UsdTransform2d = "UsdTransform2d"
35    UsdPrimvarReader = "UsdPrimvarReader"
36
37class ShaderProps(ConstantsGroup):
38    Bias = "bias"
39    Scale = "scale"
40    SourceColorSpace = "sourceColorSpace"
41    Normal = "normal"
42    File = "file"
43
44
45def _IsPackageOrPackagedLayer(layer):
46    return layer.GetFileFormat().IsPackage() or \
47           Ar.IsPackageRelativePath(layer.identifier)
48
49class BaseRuleChecker(object):
50    """This is Base class for all the rule-checkers."""
51    def __init__(self, verbose, consumerLevelChecks, assetLevelChecks):
52        self._verbose = verbose
53        self._consumerLevelChecks = consumerLevelChecks
54        self._assetLevelChecks = assetLevelChecks
55        self._failedChecks = []
56        self._errors = []
57        self._warnings = []
58
59    def _AddFailedCheck(self, msg):
60        self._failedChecks.append(msg)
61
62    def _AddError(self, msg):
63        self._errors.append(msg)
64
65    def _AddWarning(self, msg):
66        self._warnings.append(msg)
67
68    def _Msg(self, msg):
69        if self._verbose:
70            print(msg)
71
72    def GetFailedChecks(self):
73        return self._failedChecks
74
75    def GetErrors(self):
76        return self._errors
77
78    def GetWarnings(self):
79        return self._warnings
80
81    # -------------------------------------------------------------------------
82    # Virtual methods that any derived rule-checker may want to override.
83    # Default implementations do nothing.
84    #
85    # A rule-checker may choose to override one or more of the virtual methods.
86    # The callbacks are invoked in the order they are defined here (i.e.
87    # CheckStage is invoked first, followed by CheckDiagnostics, followed by
88    # CheckUnresolvedPaths and so on until CheckPrim). Some of the callbacks may
89    # be invoked multiple times per-rule with different parameters, for example,
90    # CheckLayer, CheckPrim and CheckZipFile.
91
92    def CheckStage(self, usdStage):
93        """ Check the given usdStage. """
94        pass
95
96    def CheckDiagnostics(self, diagnostics):
97        """ Check the diagnostic messages that were generated when opening the
98            USD stage. The diagnostic messages are collected using a
99            UsdUtilsCoalescingDiagnosticDelegate.
100        """
101        pass
102
103    def CheckUnresolvedPaths(self, unresolvedPaths):
104        """ Check or process any unresolved asset paths that were found when
105            analysing the dependencies.
106        """
107        pass
108
109    def CheckDependencies(self, usdStage, layerDeps, assetDeps):
110        """ Check usdStage's layer and asset dependencies that were gathered
111            using UsdUtils.ComputeAllDependencies().
112        """
113        pass
114
115    def CheckLayer(self, layer):
116        """ Check the given SdfLayer. """
117        pass
118
119    def CheckZipFile(self, zipFile, packagePath):
120        """ Check the zipFile object created by opening the package at path
121            packagePath.
122        """
123        pass
124
125    def CheckPrim(self, prim):
126        """ Check the given prim, which may only exist is a specific combination
127            of variant selections on the UsdStage.
128        """
129        pass
130
131    def ResetCaches(self):
132        """ Reset any caches the rule owns.  Called whenever stage authoring
133        occurs, such as when we iterate through VariantSet combinations.
134        """
135        pass
136
137    # -------------------------------------------------------------------------
138
139class ByteAlignmentChecker(BaseRuleChecker):
140    @staticmethod
141    def GetDescription():
142        return "Files within a usdz package must be laid out properly, "\
143                "i.e. they should be aligned to 64 bytes."
144
145    def __init__(self, verbose, consumerLevelChecks, assetLevelChecks):
146        super(ByteAlignmentChecker, self).__init__(verbose,
147                                                   consumerLevelChecks,
148                                                   assetLevelChecks)
149
150    def CheckZipFile(self, zipFile, packagePath):
151        fileNames = zipFile.GetFileNames()
152        for fileName in fileNames:
153            fileExt = Ar.GetResolver().GetExtension(fileName)
154            fileInfo = zipFile.GetFileInfo(fileName)
155            offset = fileInfo.dataOffset
156            if offset % 64 != 0:
157                self._AddFailedCheck("File '%s' in package '%s' has an "
158                        "invalid offset %s." %
159                        (fileName, packagePath, offset))
160
161class CompressionChecker(BaseRuleChecker):
162    @staticmethod
163    def GetDescription():
164        return "Files within a usdz package should not be compressed or "\
165               "encrypted."
166
167    def __init__(self, verbose, consumerLevelChecks, assetLevelChecks):
168        super(CompressionChecker, self).__init__(verbose,
169                                                 consumerLevelChecks,
170                                                 assetLevelChecks)
171
172    def CheckZipFile(self, zipFile, packagePath):
173        fileNames = zipFile.GetFileNames()
174        for fileName in fileNames:
175            fileExt = Ar.GetResolver().GetExtension(fileName)
176            fileInfo = zipFile.GetFileInfo(fileName)
177            if fileInfo.compressionMethod != 0:
178                self._AddFailedCheck("File '%s' in package '%s' has "
179                    "compression. Compression method is '%s', actual size "
180                    "is %s. Uncompressed size is %s." % (
181                    fileName, packagePath, fileInfo.compressionMethod,
182                    fileInfo.size, fileInfo.uncompressedSize))
183
184class MissingReferenceChecker(BaseRuleChecker):
185    @staticmethod
186    def GetDescription():
187        return "The composed USD stage should not contain any unresolvable"\
188            " asset dependencies (in every possible variation of the "\
189            "asset), when using the default asset resolver. "
190
191    def __init__(self, verbose, consumerLevelChecks, assetLevelChecks):
192        super(MissingReferenceChecker, self).__init__(verbose,
193                                                      consumerLevelChecks,
194                                                      assetLevelChecks)
195
196    def CheckDiagnostics(self, diagnostics):
197        for diag in diagnostics:
198            # "_ReportErrors" is the name of the function that issues
199            # warnings about unresolved references, sublayers and other
200            # composition arcs.
201            if '_ReportErrors' in diag.sourceFunction and \
202                'usd/stage.cpp' in diag.sourceFileName:
203                self._AddFailedCheck(diag.commentary)
204
205    def CheckUnresolvedPaths(self, unresolvedPaths):
206        for unresolvedPath in unresolvedPaths:
207            self._AddFailedCheck("Found unresolvable external dependency "
208                    "'%s'." % unresolvedPath)
209
210class StageMetadataChecker(BaseRuleChecker):
211    @staticmethod
212    def GetDescription():
213        return """All stages should declare their 'upAxis' and 'metersPerUnit'.
214Stages that can be consumed as referencable assets should furthermore have
215a valid 'defaultPrim' declared, and stages meant for consumer-level packaging
216should always have upAxis set to 'Y'"""
217
218    def __init__(self, verbose, consumerLevelChecks, assetLevelChecks):
219        super(StageMetadataChecker, self).__init__(verbose,
220                                                   consumerLevelChecks,
221                                                   assetLevelChecks)
222
223    def CheckStage(self, usdStage):
224        from pxr import UsdGeom
225
226        if not usdStage.HasAuthoredMetadata(UsdGeom.Tokens.upAxis):
227            self._AddFailedCheck("Stage does not specify an upAxis.")
228        elif self._consumerLevelChecks:
229            upAxis = UsdGeom.GetStageUpAxis(usdStage)
230            if upAxis != UsdGeom.Tokens.y:
231                self._AddFailedCheck("Stage specifies upAxis '%s'. upAxis should"
232                                     " be '%s'." % (upAxis, UsdGeom.Tokens.y))
233
234        if not usdStage.HasAuthoredMetadata(UsdGeom.Tokens.metersPerUnit):
235            self._AddFailedCheck("Stage does not specify its linear scale "
236                                 "in metersPerUnit.")
237
238        if self._assetLevelChecks:
239            defaultPrim = usdStage.GetDefaultPrim()
240            if not defaultPrim:
241                self._AddFailedCheck("Stage has missing or invalid defaultPrim.")
242
243
244
245class TextureChecker(BaseRuleChecker):
246    # The most basic formats are those published in the USDZ spec
247    _basicUSDZImageFormats = ("jpg", "png")
248
249    # In non-consumer-content (arkit) mode, OIIO can allow us to
250    # additionaly read other formats from USDZ packages
251    _extraUSDZ_OIIOImageFormats = (".exr")
252
253    # Include a list of "unsupported" image formats to provide better error
254    # messages when we find one of these.  Our builtin image decoder
255    # _can_ decode these, but they are not considered portable consumer-level
256    _unsupportedImageFormats = ["bmp", "tga", "hdr", "exr", "tif", "zfile",
257                                "tx"]
258
259    @staticmethod
260    def GetDescription():
261        return """Texture files should be readable by intended client
262(only .jpg or .png for consumer-level USDZ)."""
263
264    def __init__(self, verbose, consumerLevelChecks, assetLevelChecks):
265        # Check if the prim has an allowed type.
266        super(TextureChecker, self).__init__(verbose, consumerLevelChecks, assetLevelChecks)
267        # a None value for _allowedFormats indicates all formats are allowed
268        self._allowedFormats = None
269
270    def CheckStage(self, usdStage):
271        # This is the point at which we can determine whether we have a USDZ
272        # archive, and so have enough information to set the allowed formats.
273        rootLayer = usdStage.GetRootLayer()
274        if rootLayer.GetFileFormat().IsPackage() or self._consumerLevelChecks:
275            self._allowedFormats = list(TextureChecker._basicUSDZImageFormats)
276            if not self._consumerLevelChecks:
277                self._allowedFormats.append(TextureChecker._extraUSDZ_OIIOImageFormats)
278        else:
279            self._Msg("Not performing texture format checks for general "
280                      "USD asset")
281
282
283    def _CheckTexture(self, texAssetPath, inputPath):
284        self._Msg("Checking texture <%s>." % texAssetPath)
285        texFileExt = Ar.GetResolver().GetExtension(texAssetPath).lower()
286        if (self._consumerLevelChecks and
287            texFileExt in TextureChecker._unsupportedImageFormats):
288            self._AddFailedCheck("Texture <%s> with asset @%s@ has non-portable "
289                                 "file format." % (inputPath, texAssetPath))
290        elif texFileExt not in self._allowedFormats:
291            self._AddFailedCheck("Texture <%s> with asset @%s@ has unknown "
292                                 "file format." % (inputPath, texAssetPath))
293
294    def CheckPrim(self, prim):
295        # Right now, we find texture referenced by looking at the asset-valued
296        # inputs on Connectable prims.
297        from pxr import Sdf, Usd, UsdShade
298
299        # Nothing to do if we are allowing all formats, or if
300        # we are an untyped prim
301        if self._allowedFormats is None or not prim.GetTypeName():
302            return
303
304        # Check if the prim is Connectable.
305        connectable = UsdShade.ConnectableAPI(prim)
306        if not connectable:
307            return
308
309        shaderInputs = connectable.GetInputs()
310        for ip in shaderInputs:
311            attrPath = ip.GetAttr().GetPath()
312            if ip.GetTypeName() == Sdf.ValueTypeNames.Asset:
313                texFilePath = ip.Get(Usd.TimeCode.EarliestTime())
314                # ip may be unauthored and/or connected
315                if texFilePath:
316                    self._CheckTexture(texFilePath.path, attrPath)
317            elif ip.GetTypeName() == Sdf.ValueTypeNames.AssetArray:
318                texPathArray = ip.Get(Usd.TimeCode.EarliestTime())
319                if texPathArray:
320                    for texPath in texPathArray:
321                        self._CheckTexture(texPath, attrPath)
322
323class PrimEncapsulationChecker(BaseRuleChecker):
324    @staticmethod
325    def GetDescription():
326        return """Check for basic prim encapsulation rules:
327   - Boundables may not be nested under Gprims
328   - Connectable prims (e.g. Shader, Material, etc) can only be nested
329     inside other Container-like Connectable prims. Container-like prims
330     include Material, NodeGraph, Light, LightFilter, and *exclude Shader*"""
331
332    def __init__(self, verbose, consumerLevelChecks, assetLevelChecks):
333        super(PrimEncapsulationChecker, self).__init__(verbose,
334                                                       consumerLevelChecks,
335                                                       assetLevelChecks)
336        self.ResetCaches()
337
338    def _HasGprimAncestor(self, prim):
339        from pxr import Sdf, UsdGeom
340        path = prim.GetPath()
341        if path in self._hasGprimInPathMap:
342            return self._hasGprimInPathMap[path]
343        elif path == Sdf.Path.absoluteRootPath:
344            self._hasGprimInPathMap[path] = False
345            return False
346        else:
347            val = (self._HasGprimAncestor(prim.GetParent()) or
348                   prim.IsA(UsdGeom.Gprim))
349            self._hasGprimInPathMap[path] = val
350            return val
351
352    def _FindConnectableAncestor(self, prim):
353        from pxr import Sdf, UsdShade
354        path = prim.GetPath()
355        if path in self._connectableAncestorMap:
356            return self._connectableAncestorMap[path]
357        elif path == Sdf.Path.absoluteRootPath:
358            self._connectableAncestorMap[path] = None
359            return None
360        else:
361            val = self._FindConnectableAncestor(prim.GetParent())
362            # The GetTypeName() check is to work around a bug in
363            # ConnectableAPIBehavior registry.
364            if prim.GetTypeName() and not val:
365                conn = UsdShade.ConnectableAPI(prim)
366                if conn:
367                    val = prim
368            self._connectableAncestorMap[path] = val
369            return val
370
371
372    def CheckPrim(self, prim):
373        from pxr import UsdGeom, UsdShade
374
375        parent = prim.GetParent()
376
377        # Of course we must allow Boundables under other Boundables, so that
378        # schemas like UsdGeom.Pointinstancer can nest their prototypes.  But
379        # we disallow a PointInstancer under a Mesh just as we disallow a Mesh
380        # under a Mesh, for the same reason: we cannot then independently
381        # adjust visibility for the two objects, nor can we reasonably compute
382        # the parent Mesh's extent.
383        if prim.IsA(UsdGeom.Boundable):
384            if parent:
385                if self._HasGprimAncestor(parent):
386                    self._AddFailedCheck("Gprim <%s> has an ancestor prim that "
387                                         "is also a Gprim, which is not allowed."
388                                         % prim.GetPath())
389
390        connectable = UsdShade.ConnectableAPI(prim)
391        # The GetTypeName() check is to work around a bug in
392        # ConnectableAPIBehavior registry.
393        if prim.GetTypeName() and connectable:
394            if parent:
395                pConnectable = UsdShade.ConnectableAPI(parent)
396                if not parent.GetTypeName():
397                    pConnectable = None
398                if pConnectable and not pConnectable.IsContainer():
399                    # XXX This should be a failure as it is a violation of the
400                    # UsdShade OM.  But pragmatically, there are many
401                    # authoring tools currently producing this structure, which
402                    # does not _currently_ perturb Hydra, so we need to start
403                    # with a warning
404                    self._AddWarning("Connectable %s <%s> cannot reside "
405                                     "under a non-Container Connectable %s"
406                                     % (prim.GetTypeName(),
407                                        prim.GetPath(),
408                                        parent.GetTypeName()))
409                elif not pConnectable:
410                    # it's only OK to have a non-connectable parent if all
411                    # the rest of your ancestors are also non-connectable.  The
412                    # error message we give is targeted at the most common
413                    # infraction, using Scope or other grouping prims inside
414                    # a Container like a Material
415                    connAnstr = self._FindConnectableAncestor(parent)
416                    if connAnstr is not None:
417                        self.AddFailedCheck("Connectable %s <%s> can only have "
418                                            "Connectable Container ancestors up "
419                                            "to %s ancestor <%s>, but its parent"
420                                            "%s is a %s." %
421                                            (prim.GetTypeName(),
422                                             prim.GetPath(),
423                                             connAnstr.GetTypeName(),
424                                             connAnstr.GetPath(),
425                                             parent.GetName(),
426                                             parent.GetTypeName()))
427
428
429    def ResetCaches(self):
430        self._connectableAncestorMap = dict()
431        self._hasGprimInPathMap = dict()
432
433
434
435class NormalMapTextureChecker(BaseRuleChecker):
436    @staticmethod
437    def GetDescription():
438        return """UsdUVTexture nodes that feed the _inputs:normals_ of a
439UsdPreviewSurface must ensure that the data is encoded and scaled properly.
440Specifically:
441   - Since normals are expected to be in the range [(-1,-1,-1), (1,1,1)],
442     the Texture node must transform 8-bit textures from their [0..1] range by
443     setting its _inputs:scale_ to [2, 2, 2, 1] and
444     _inputs:bias_ to [-1, -1, -1, 0]
445   - Normal map data is commonly expected to be linearly encoded.  However, many
446     image-writing tools automatically set the profile of three-channel, 8-bit
447     images to SRGB.  To prevent an unwanted transformation, the UsdUVTexture's
448     _inputs:sourceColorSpace_ must be set to "raw".  This program cannot
449     currently read the texture metadata itself, so for now we emit warnings
450     about this potential infraction for all 8 bit image formats.
451"""
452    def __init__(self, verbose, consumerLevelChecks, assetLevelChecks):
453        super(NormalMapTextureChecker, self).__init__(verbose,
454                                                      consumerLevelChecks,
455                                                      assetLevelChecks)
456
457    def _GetShaderId(self, shader):
458        # We might someday try harder to find an identifier...
459        return shader.GetShaderId()
460
461    def _TextureIs8Bit(self, asset):
462        # Eventually we hope to leverage HioImage through a plugin system,
463        # when Imaging is present, to answer this and other image queries
464        # more definitively
465        from pxr import Ar
466        ext = Ar.GetResolver().GetExtension(asset.resolvedPath)
467        # not an exhaustive list, but ones we typically can read
468        return ext in ["bmp", "tga", "jpg", "png", "tif"]
469
470    def _GetInputValue(self, shader, inputName):
471        from pxr import Usd
472        input = shader.GetInput(inputName)
473        if not input:
474            return None
475        return input.Get(Usd.TimeCode.EarliestTime())
476
477    def CheckPrim(self, prim):
478        from pxr import UsdShade, Gf
479        from pxr.UsdShade import Utils as ShadeUtils
480
481        if not prim.IsA(UsdShade.Shader):
482            return
483
484        shader = UsdShade.Shader(prim)
485        if not shader:
486            self._AddError("Invalid shader prim <%s>." % prim.GetPath())
487            return
488
489        shaderId = self._GetShaderId(shader)
490
491        # We may have failed to fetch an identifier for asset/source-based
492        # nodes. We are only interested in UsdPreviewSurface nodes identified via
493        # info:id, so it's not an error
494        if not shaderId or shaderId != NodeTypes.UsdPreviewSurface:
495            return
496
497        normalInput = shader.GetInput(ShaderProps.Normal)
498        if not normalInput:
499            return
500        valueProducingAttrs = ShadeUtils.GetValueProducingAttributes(normalInput)
501        if not valueProducingAttrs or valueProducingAttrs[0].GetPrim() == prim:
502            return
503
504        sourcePrim = valueProducingAttrs[0].GetPrim()
505
506        sourceShader = UsdShade.Shader(sourcePrim)
507        if not sourceShader:
508            # In theory, could be connected to an interface attribute of a
509            # parent connectable... not useful, but not an error
510            if UsdShade.ConnectableAPI(sourcePrim):
511                return
512            self._AddFailedCheck("%s.%s on prim <%s> is connected to a"
513                                 " non-Shader prim." % \
514                                 (NodeTypes.UsdPreviewSurface,
515                                  ShaderProps.Normal))
516            return
517
518        sourceId = self._GetShaderId(sourceShader)
519
520        # We may have failed to fetch an identifier for asset/source-based
521        # nodes. OR, we could potentially be driven by a UsdPrimvarReader,
522        # in which case we'd have nothing to validate
523        if not sourceId or sourceId != NodeTypes.UsdUVTexture:
524            return
525
526        texAsset = self._GetInputValue(sourceShader, ShaderProps.File)
527
528        if not texAsset or not texAsset.resolvedPath:
529            self._AddFailedCheck("%s prim <%s> has invalid or unresolvable "
530                                 "inputs:file of @%s@" % \
531                                 (NodeTypes.UsdUVTexture,
532                                  sourcePrim.GetPath(),
533                                  texAsset.path if texAsset else ""))
534            return
535
536        if not self._TextureIs8Bit(texAsset):
537            # really nothing more is required for image depths > 8 bits,
538            # which we assume FOR NOW, are floating point
539            return
540
541        if not self._GetInputValue(sourceShader, ShaderProps.SourceColorSpace):
542            self._AddWarning("%s prim <%s> that reads Normal Map @%s@ may need "
543                             "to set inputs:sourceColorSpace to 'raw' as some "
544                             "8-bit image writers always indicate an SRGB "
545                             "encoding." % \
546                             (NodeTypes.UsdUVTexture,
547                              sourcePrim.GetPath(),
548                              texAsset.path))
549
550        bias = self._GetInputValue(sourceShader, ShaderProps.Bias)
551
552        scale = self._GetInputValue(sourceShader, ShaderProps.Scale)
553
554        if not (bias and scale and
555                isinstance(bias, Gf.Vec4f) and isinstance(scale, Gf.Vec4f)):
556            # XXX This should be a failure, as it results in broken normal
557            # maps in Storm and hdPrman, at least.  But for the same reason
558            # as the shader-under-shader check, we cannot fail until at least
559            # the major authoring tools have been updated.
560            self._AddWarning("%s prim <%s> reads 8 bit Normal Map @%s@, "
561                             "which requires that inputs:scale be set to "
562                             "[2, 2, 2, 1] and inputs:bias be set to "
563                             "[-1, -1, -1, 0] for proper interpretation." %\
564                             (NodeTypes.UsdUVTexture,
565                              sourcePrim.GetPath(),
566                              texAsset.path))
567            return
568
569        # don't really care about fourth components...
570        if (bias[0] != -1 or bias[1] != -1 or bias[2] != -1 or
571            scale[0] != 2 or scale[1] != 2 or scale[2] != 2):
572            self._AddWarning("%s prim <%s> reads an 8 bit Normal Map, "
573                             "but has non-standard inputs:scale and "
574                             "inputs:bias values of %s and %s" %\
575                             (NodeTypes.UsdUVTexture,
576                              sourcePrim.GetPath(),
577                              str(scale), str(bias)))
578
579
580class ARKitPackageEncapsulationChecker(BaseRuleChecker):
581    @staticmethod
582    def GetDescription():
583        return "If the root layer is a package, then the composed stage "\
584               "should not contain references to files outside the package. "\
585               "In other words, the package should be entirely self-contained."
586
587    def __init__(self, verbose, consumerLevelChecks, assetLevelChecks):
588        super(ARKitPackageEncapsulationChecker, self).\
589            __init__(verbose, consumerLevelChecks, assetLevelChecks)
590
591    def CheckDependencies(self, usdStage, layerDeps, assetDeps):
592        rootLayer = usdStage.GetRootLayer()
593        if not _IsPackageOrPackagedLayer(rootLayer):
594            return
595
596        packagePath = usdStage.GetRootLayer().realPath
597        if packagePath:
598            if Ar.IsPackageRelativePath(packagePath):
599                packagePath = Ar.SplitPackageRelativePathOuter(
600                        packagePath)[0]
601            for layer in layerDeps:
602                # In-memory layers like session layers (which we must skip when
603                # doing this check) won't have a real path.
604                if layer.realPath:
605                    if not layer.realPath.startswith(packagePath):
606                        self._AddFailedCheck("Found loaded layer '%s' that "
607                            "does not belong to the package '%s'." %
608                            (layer.identifier, packagePath))
609            for asset in assetDeps:
610                if not asset.startswith(packagePath):
611                    self._AddFailedCheck("Found asset reference '%s' that "
612                        "does not belong to the package '%s'." %
613                        (asset, packagePath))
614
615class ARKitLayerChecker(BaseRuleChecker):
616    # Only core USD file formats are allowed.
617    _allowedLayerFormatIds = ('usd', 'usda', 'usdc', 'usdz')
618
619    @staticmethod
620    def GetDescription():
621        return "All included layers that participate in composition should"\
622            " have one of the core supported file formats."
623
624    def __init__(self, verbose, consumerLevelChecks, assetLevelChecks):
625        # Check if the prim has an allowed type.
626        super(ARKitLayerChecker, self).__init__(verbose,
627                                                consumerLevelChecks,
628                                                assetLevelChecks)
629
630    def CheckLayer(self, layer):
631        self._Msg("Checking layer <%s>." % layer.identifier)
632
633        formatId = layer.GetFileFormat().formatId
634        if not formatId in \
635            ARKitLayerChecker._allowedLayerFormatIds:
636            self._AddFailedCheck("Layer '%s' has unsupported formatId "
637                    "'%s'." % (layer.identifier, formatId))
638
639class ARKitPrimTypeChecker(BaseRuleChecker):
640    # All core prim types other than UsdGeomPointInstancers, Curve types, Nurbs,
641    # and the types in  UsdLux are allowed.
642    _allowedPrimTypeNames = ('', 'Scope', 'Xform', 'Camera',
643                            'Shader', 'Material',
644                            'Mesh', 'Sphere', 'Cube', 'Cylinder', 'Cone',
645                            'Capsule', 'GeomSubset', 'Points',
646                            'SkelRoot', 'Skeleton', 'SkelAnimation',
647                            'BlendShape', 'SpatialAudio')
648
649    @staticmethod
650    def GetDescription():
651        return "UsdGeomPointInstancers and custom schemas not provided by "\
652                "core USD are not allowed."
653
654    def __init__(self, verbose, consumerLevelChecks, assetLevelChecks):
655        # Check if the prim has an allowed type.
656        super(ARKitPrimTypeChecker, self).__init__(verbose,
657                                                   consumerLevelChecks,
658                                                   assetLevelChecks)
659
660    def CheckPrim(self, prim):
661        self._Msg("Checking prim <%s>." % prim.GetPath())
662        if prim.GetTypeName() not in \
663            ARKitPrimTypeChecker._allowedPrimTypeNames:
664            self._AddFailedCheck("Prim <%s> has unsupported type '%s'." %
665                                    (prim.GetPath(), prim.GetTypeName()))
666
667class ARKitShaderChecker(BaseRuleChecker):
668    @staticmethod
669    def GetDescription():
670        return "Shader nodes must have \"id\" as the implementationSource, "  \
671               "with id values that begin with \"Usd*\". Also, shader inputs "\
672               "with connections must each have a single, valid connection "  \
673               "source."
674
675    def __init__(self, verbose, consumerLevelChecks, assetLevelChecks):
676        super(ARKitShaderChecker, self).__init__(verbose, consumerLevelChecks,
677                                                 assetLevelChecks)
678
679    def CheckPrim(self, prim):
680        from pxr import UsdShade
681
682        if not prim.IsA(UsdShade.Shader):
683            return
684
685        shader = UsdShade.Shader(prim)
686        if not shader:
687            # Error has already been issued by a Base-level checker
688            return
689
690        self._Msg("Checking shader <%s>." % prim.GetPath())
691
692        implSource = shader.GetImplementationSource()
693        if implSource != UsdShade.Tokens.id:
694            self._AddFailedCheck("Shader <%s> has non-id implementation "
695                    "source '%s'." % (prim.GetPath(), implSource))
696
697        shaderId = shader.GetShaderId()
698        if not shaderId or \
699           not (shaderId in [NodeTypes.UsdPreviewSurface,
700                             NodeTypes.UsdUVTexture,
701                             NodeTypes.UsdTransform2d] or
702                shaderId.startswith(NodeTypes.UsdPrimvarReader)) :
703            self._AddFailedCheck("Shader <%s> has unsupported info:id '%s'."
704                    % (prim.GetPath(), shaderId))
705
706        # Check shader input connections
707        shaderInputs = shader.GetInputs()
708        for shdInput in shaderInputs:
709            connections = shdInput.GetAttr().GetConnections()
710            # If an input has one or more connections, ensure that the
711            # connections are valid.
712            if len(connections) > 0:
713                if len(connections) > 1:
714                    self._AddFailedCheck("Shader input <%s> has %s connection "
715                        "sources, but only one is allowed." %
716                        (shdInput.GetAttr.GetPath(), len(connections)))
717                connectedSource = shdInput.GetConnectedSource()
718                if connectedSource is None:
719                    self._AddFailedCheck("Connection source <%s> for shader "
720                        "input <%s> is missing." % (connections[0],
721                        shdInput.GetAttr().GetPath()))
722                else:
723                    # The source must be a valid shader or material prim.
724                    source = connectedSource[0]
725                    if not source.GetPrim().IsA(UsdShade.Shader) and \
726                       not source.GetPrim().IsA(UsdShade.Material):
727                        self._AddFailedCheck("Shader input <%s> has an invalid "
728                            "connection source prim of type '%s'." %
729                            (shdInput.GetAttr().GetPath(),
730                             source.GetPrim().GetTypeName()))
731
732class ARKitMaterialBindingChecker(BaseRuleChecker):
733    @staticmethod
734    def GetDescription():
735        return "All material binding relationships must have valid targets."
736
737    def __init__(self, verbose, consumerLevelChecks, assetLevelChecks):
738        super(ARKitMaterialBindingChecker, self).__init__(verbose,
739                                                          consumerLevelChecks,
740                                                          assetLevelChecks)
741
742    def CheckPrim(self, prim):
743        from pxr import UsdShade
744        relationships = prim.GetRelationships()
745        bindingRels = [rel for rel in relationships if
746                rel.GetName().startswith(UsdShade.Tokens.materialBinding)]
747
748        for bindingRel in bindingRels:
749            targets = bindingRel.GetTargets()
750            if len(targets) == 1:
751                directBinding = UsdShade.MaterialBindingAPI.DirectBinding(
752                        bindingRel)
753                if not directBinding.GetMaterial():
754                    self._AddFailedCheck("Direct material binding <%s> targets "
755                        "an invalid material <%s>." % (bindingRel.GetPath(),
756                        directBinding.GetMaterialPath()))
757            elif len(targets) == 2:
758                collBinding = UsdShade.MaterialBindingAPI.CollectionBinding(
759                        bindingRel)
760                if not collBinding.GetMaterial():
761                    self._AddFailedCheck("Collection-based material binding "
762                        "<%s> targets an invalid material <%s>." %
763                        (bindingRel.GetPath(), collBinding.GetMaterialPath()))
764                if not collBinding.GetCollection():
765                    self._AddFailedCheck("Collection-based material binding "
766                        "<%s> targets an invalid collection <%s>." %
767                        (bindingRel.GetPath(), collBinding.GetCollectionPath()))
768
769class ARKitFileExtensionChecker(BaseRuleChecker):
770    _allowedFileExtensions = \
771        ARKitLayerChecker._allowedLayerFormatIds + \
772        TextureChecker._basicUSDZImageFormats
773
774    @staticmethod
775    def GetDescription():
776        return "Only layer files and textures are allowed in a package."
777
778    def __init__(self, verbose, consumerLevelChecks, assetLevelChecks):
779        super(ARKitFileExtensionChecker, self).__init__(verbose,
780                                                        consumerLevelChecks,
781                                                        assetLevelChecks)
782
783    def CheckZipFile(self, zipFile, packagePath):
784        fileNames = zipFile.GetFileNames()
785        for fileName in fileNames:
786            fileExt = Ar.GetResolver().GetExtension(fileName)
787            if fileExt not in ARKitFileExtensionChecker._allowedFileExtensions:
788                self._AddFailedCheck("File '%s' in package '%s' has an "
789                    "unknown or unsupported extension '%s'." %
790                    (fileName, packagePath, fileExt))
791
792class ARKitRootLayerChecker(BaseRuleChecker):
793    @staticmethod
794    def GetDescription():
795        return "The root layer of the package must be a usdc file and " \
796            "must not include any external dependencies that participate in "\
797            "stage composition."
798
799    def __init__(self, verbose, consumerLevelChecks, assetLevelChecks):
800        super(ARKitRootLayerChecker, self).__init__(verbose,
801                                                    consumerLevelChecks,
802                                                    assetLevelChecks)
803
804    def CheckStage(self, usdStage):
805        usedLayers = usdStage.GetUsedLayers()
806        # This list excludes any session layers.
807        usedLayersOnDisk = [i for i in usedLayers if i.realPath]
808        if len(usedLayersOnDisk) > 1:
809            self._AddFailedCheck("The stage uses %s layers. It should "
810                "contain a single usdc layer to be compatible with ARKit's "
811                "implementation of usdz." % len(usedLayersOnDisk))
812
813        rootLayerRealPath = usdStage.GetRootLayer().realPath
814        if rootLayerRealPath.endswith(".usdz"):
815            # Check if the root layer in the package is a usdc.
816            from pxr import Usd
817            zipFile = Usd.ZipFile.Open(rootLayerRealPath)
818            if not zipFile:
819                self._AddError("Could not open package at path '%s'." %
820                        resolvedPath)
821                return
822            fileNames = zipFile.GetFileNames()
823            if not fileNames[0].endswith(".usdc"):
824                self._AddFailedCheck("First file (%s) in usdz package '%s' "
825                    "does not have the .usdc extension." % (fileNames[0],
826                    rootLayerRealPath))
827        elif not rootLayerRealPath.endswith(".usdc"):
828            self._AddFailedCheck("Root layer of the stage '%s' does not "
829                "have the '.usdc' extension." % (rootLayerRealPath))
830
831class ComplianceChecker(object):
832    """ A utility class for checking compliance of a given USD asset or a USDZ
833    package.
834
835    Since usdz files are zip files, someone could use generic zip tools to
836    create an archive and just change the extension, producing a .usdz file that
837    does not honor the additional constraints that usdz files require.  Even if
838    someone does use our official archive creation tools, though, we
839    intentionally allow creation of usdz files that can be very "permissive" in
840    their contents for internal studio uses, where portability outside the
841    studio is not a concern.  For content meant to be delivered over the web
842    (eg. ARKit assets), however, we must be much more restrictive.
843
844    This class provides two levels of compliance checking:
845    * "structural" validation that is represented by a set of base rules.
846    * "ARKit" compatibility validation, which includes many more restrictions.
847
848    Calling ComplianceChecker.DumpAllRules() will print an enumeration of the
849    various rules in the two categories of compliance checking.
850    """
851
852    @staticmethod
853    def GetBaseRules():
854        return [ByteAlignmentChecker, CompressionChecker,
855                MissingReferenceChecker, StageMetadataChecker, TextureChecker,
856                PrimEncapsulationChecker, NormalMapTextureChecker]
857
858    @staticmethod
859    def GetARKitRules(skipARKitRootLayerCheck=False):
860        arkitRules = [ARKitLayerChecker, ARKitPrimTypeChecker,
861                      ARKitShaderChecker,
862                      ARKitMaterialBindingChecker,
863                      ARKitFileExtensionChecker,
864                      ARKitPackageEncapsulationChecker]
865        if not skipARKitRootLayerCheck:
866            arkitRules.append(ARKitRootLayerChecker)
867        return arkitRules
868
869    @staticmethod
870    def GetRules(arkit=False, skipARKitRootLayerCheck=False):
871        allRules = ComplianceChecker.GetBaseRules()
872        if arkit:
873            arkitRules = ComplianceChecker.GetARKitRules(
874                    skipARKitRootLayerCheck=skipARKitRootLayerCheck)
875            allRules += arkitRules
876        return allRules
877
878    @staticmethod
879    def DumpAllRules():
880        print('Base rules:')
881        for ruleNum, rule in enumerate(GetBaseRules()):
882            print('[%s] %s' % (ruleNum + 1, rule.GetDescription()))
883        print('-' * 30)
884        print('ARKit rules: ')
885        for ruleNum, rule in enumerate(GetBaseRules()):
886            print('[%s] %s' % (ruleNum + 1, rule.GetDescription()))
887        print('-' * 30)
888
889    def __init__(self, arkit=False, skipARKitRootLayerCheck=False,
890                 rootPackageOnly=False, skipVariants=False, verbose=False,
891                 assetLevelChecks=True):
892        self._rootPackageOnly = rootPackageOnly
893        self._doVariants = not skipVariants
894        self._verbose = verbose
895        self._errors = []
896        self._warnings = []
897
898        # Once a package has been checked, it goes into this set.
899        self._checkedPackages = set()
900
901        # Instantiate an instance of every rule checker and store in a list.
902        self._rules = [Rule(self._verbose, arkit, assetLevelChecks) for Rule in
903                ComplianceChecker.GetRules(arkit, skipARKitRootLayerCheck)]
904
905    def _Msg(self, msg):
906        if self._verbose:
907            print(msg)
908
909    def _AddError(self, errMsg):
910        self._errors.append(errMsg)
911
912    def _AddWarning(self, errMsg):
913        self._warnings.append(errMsg)
914
915    def GetErrors(self):
916        errors = self._errors
917        for rule in self._rules:
918            errs = rule.GetErrors()
919            for err in errs:
920                errors.append("Error checking rule '%s': %s" %
921                    (type(rule).__name__, err))
922        return errors
923
924    def GetWarnings(self):
925        warnings = self._warnings
926        for rule in self._rules:
927            advisories = rule.GetWarnings()
928            for ad in advisories:
929                warnings.append("%s (may violate '%s')" % (ad,
930                        type(rule).__name__))
931        return warnings
932
933    def DumpRules(self):
934        print('Checking rules: ')
935        for rule in self._rules:
936            print('-' * 30)
937            print('[%s]:\n %s' % (type(rule).__name__, rule.GetDescription()))
938        print('-' * 30)
939
940    def GetFailedChecks(self):
941        failedChecks = []
942        for rule in self._rules:
943            fcs = rule.GetFailedChecks()
944            for fc in fcs:
945                failedChecks.append("%s (fails '%s')" % (fc,
946                        type(rule).__name__))
947        return failedChecks
948
949    def CheckCompliance(self, inputFile):
950        from pxr import Sdf, Usd, UsdUtils
951        if not Usd.Stage.IsSupportedFile(inputFile):
952            _AddError("Cannot open file '%s' on a USD stage." % args.inputFile)
953            return
954
955        # Collect all warnings using a diagnostic delegate.
956        delegate = UsdUtils.CoalescingDiagnosticDelegate()
957        usdStage = Usd.Stage.Open(inputFile)
958        stageOpenDiagnostics = delegate.TakeUncoalescedDiagnostics()
959
960        for rule in self._rules:
961            rule.CheckStage(usdStage)
962            rule.CheckDiagnostics(stageOpenDiagnostics)
963
964        with Ar.ResolverContextBinder(usdStage.GetPathResolverContext()):
965            # This recursively computes all of inputFiles's external
966            # dependencies.
967            (allLayers, allAssets, unresolvedPaths) = \
968                    UsdUtils.ComputeAllDependencies(Sdf.AssetPath(inputFile))
969            for rule in self._rules:
970                rule.CheckUnresolvedPaths(unresolvedPaths)
971                rule.CheckDependencies(usdStage, allLayers, allAssets)
972
973            if self._rootPackageOnly:
974                rootLayer = usdStage.GetRootLayer()
975                if rootLayer.GetFileFormat().IsPackage():
976                    packagePath = Ar.SplitPackageRelativePathInner(
977                            rootLayer.identifier)[0]
978                    self._CheckPackage(packagePath)
979                else:
980                    self._AddError("Root layer of the USD stage (%s) doesn't belong to "
981                        "a package, but 'rootPackageOnly' is True!" %
982                        Usd.Describe(usdStage))
983            else:
984                # Process every package just once by storing them all in a set.
985                packages = set()
986                for layer in allLayers:
987                    if _IsPackageOrPackagedLayer(layer):
988                        packagePath = Ar.SplitPackageRelativePathInner(
989                                layer.identifier)[0]
990                        packages.add(packagePath)
991                    self._CheckLayer(layer)
992                for package in packages:
993                    self._CheckPackage(package)
994
995                # Traverse the entire stage and check every prim.
996                from pxr import Usd
997                # Author all variant switches in the session layer.
998                usdStage.SetEditTarget(usdStage.GetSessionLayer())
999                allPrimsIt = iter(Usd.PrimRange.Stage(usdStage,
1000                        Usd.TraverseInstanceProxies()))
1001                self._TraverseRange(allPrimsIt, isStageRoot=True)
1002
1003    def _CheckPackage(self, packagePath):
1004        self._Msg("Checking package <%s>." % packagePath)
1005
1006        # XXX: Should we open the package on a stage to ensure that it is valid
1007        # and entirely self-contained.
1008
1009        from pxr import Usd
1010        pkgExt = Ar.GetResolver().GetExtension(packagePath)
1011        if pkgExt != "usdz":
1012            self._AddError("Package at path %s has an invalid extension."
1013                           % packagePath)
1014            return
1015
1016        # Check the parent package first.
1017        if Ar.IsPackageRelativePath(packagePath):
1018            parentPackagePath = Ar.SplitPackageRelativePathInner(packagePath)[0]
1019            self._CheckPackage(parentPackagePath)
1020
1021        # Avoid checking the same parent package multiple times.
1022        if packagePath in self._checkedPackages:
1023            return
1024        self._checkedPackages.add(packagePath)
1025
1026        resolvedPath = Ar.GetResolver().Resolve(packagePath)
1027        if not resolvedPath:
1028            self._AddError("Failed to resolve package path '%s'." % packagePath)
1029            return
1030
1031        zipFile = Usd.ZipFile.Open(resolvedPath)
1032        if not zipFile:
1033            self._AddError("Could not open package at path '%s'." %
1034                           resolvedPath)
1035            return
1036        for rule in self._rules:
1037            rule.CheckZipFile(zipFile, packagePath)
1038
1039    def _CheckLayer(self, layer):
1040        for rule in self._rules:
1041            rule.CheckLayer(layer)
1042
1043    def _CheckPrim(self, prim):
1044        for rule in self._rules:
1045            rule.CheckPrim(prim)
1046
1047    def _TraverseRange(self, primRangeIt, isStageRoot):
1048        primsWithVariants = []
1049        rootPrim = primRangeIt.GetCurrentPrim()
1050        for prim in primRangeIt:
1051            # Skip variant set check on the root prim if it is the stage'.
1052            if not self._doVariants or (not isStageRoot and prim == rootPrim):
1053                self._CheckPrim(prim)
1054                continue
1055
1056            vSets = prim.GetVariantSets()
1057            vSetNames = vSets.GetNames()
1058            if len(vSetNames) == 0:
1059                self._CheckPrim(prim)
1060            else:
1061                primsWithVariants.append(prim)
1062                primRangeIt.PruneChildren()
1063
1064        for prim in primsWithVariants:
1065            self._TraverseVariants(prim)
1066
1067    def _TraverseVariants(self, prim):
1068        from pxr import Usd
1069        if prim.IsInstanceProxy():
1070            return True
1071
1072        vSets = prim.GetVariantSets()
1073        vSetNames = vSets.GetNames()
1074        allVariantNames = []
1075        for vSetName in vSetNames:
1076            vSet = vSets.GetVariantSet(vSetName)
1077            vNames = vSet.GetVariantNames()
1078            allVariantNames.append(vNames)
1079
1080        import itertools
1081        allVariations = itertools.product(*allVariantNames)
1082
1083        for variation in allVariations:
1084            self._Msg("Testing variation %s of prim <%s>" %
1085                      (variation, prim.GetPath()))
1086            for (idx, sel) in enumerate(variation):
1087                vSets.SetSelection(vSetNames[idx], sel)
1088            for rule in self._rules:
1089                rule.ResetCaches()
1090            primRangeIt = iter(Usd.PrimRange(prim,
1091                    Usd.TraverseInstanceProxies()))
1092            self._TraverseRange(primRangeIt, isStageRoot=False)
1093