Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions Sources/VRMKit/BinaryGLTF.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,26 @@ package extension BinaryGLTF {
}

extension BinaryGLTF {
public init(jsonDataOnly data: Data) throws {
var offset = MemoryLayout<UInt32>.size // skip `magic`
let rawVersion: UInt32 = try read(data, offset: &offset, size: MemoryLayout<UInt32>.size)
guard let version = GLTF.Version(rawValue: rawVersion), version == .two else {
throw VRMError.notSupportedVersion(rawVersion)
}
self.version = version

_ = try read(data, offset: &offset, size: MemoryLayout<UInt32>.size) as UInt32
let chunk0Length: UInt32 = try read(data, offset: &offset, size: MemoryLayout<UInt32>.size)
let chunk0Type: UInt32 = try read(data, offset: &offset, size: MemoryLayout<UInt32>.size)
guard ChunkType(rawValue: chunk0Type) == .json else {
throw VRMError.notSupportedChunkType(chunk0Type)
}
let jsonData = read(data, offset: &offset, size: Int(chunk0Length))
let decoder = JSONDecoder()
self.jsonData = try decoder.decode(GLTF.self, from: jsonData)
binaryBuffer = nil
}
Comment on lines +35 to +53

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

There is significant code duplication between init(jsonDataOnly:) and the existing init(data:). We can refactor this by introducing a private initializer that handles both cases based on a boolean flag. This eliminates duplication, improves maintainability, and ensures that any future changes to the GLB parsing logic only need to be made in one place.

After applying this suggestion, you can also refactor the existing init(data:) to delegate to the private initializer:

public init(data: Data) throws {
    try self.init(data: data, jsonDataOnly: false)
}
    public init(jsonDataOnly data: Data) throws {
        try self.init(data: data, jsonDataOnly: true)
    }

    private init(data: Data, jsonDataOnly: Bool) throws {
        var offset = MemoryLayout<UInt32>.size // skip `magic`
        let rawVersion: UInt32 = try read(data, offset: &offset, size: MemoryLayout<UInt32>.size)
        guard let version = GLTF.Version(rawValue: rawVersion), version == .two else {
            throw VRMError.notSupportedVersion(rawVersion)
        }
        self.version = version

        let length: UInt32 = try read(data, offset: &offset, size: MemoryLayout<UInt32>.size)
        let chunk0Length: UInt32 = try read(data, offset: &offset, size: MemoryLayout<UInt32>.size)
        let chunk0Type: UInt32 = try read(data, offset: &offset, size: MemoryLayout<UInt32>.size)
        guard ChunkType(rawValue: chunk0Type) == .json else {
            throw VRMError.notSupportedChunkType(chunk0Type)
        }
        let jsonData = read(data, offset: &offset, size: Int(chunk0Length))
        let decoder = JSONDecoder()
        self.jsonData = try decoder.decode(GLTF.self, from: jsonData)

        if !jsonDataOnly, length > offset {
            let chunk1Length: UInt32 = try read(data, offset: &offset, size: MemoryLayout<UInt32>.size)
            let chunk1Type: UInt32 = try read(data, offset: &offset, size: MemoryLayout<UInt32>.size)
            guard ChunkType(rawValue: chunk1Type) == .bin else {
                throw VRMError.notSupportedChunkType(chunk1Type)
            }
            binaryBuffer = read(data, offset: &offset, size: Int(chunk1Length)) as Data
        } else {
            binaryBuffer = nil
        }
    }


public init(data: Data) throws {
var offset = MemoryLayout<UInt32>.size // skip `magic`
let rawVersion: UInt32 = try read(data, offset: &offset, size: MemoryLayout<UInt32>.size)
Expand Down
20 changes: 20 additions & 0 deletions Sources/VRMKit/VRMLoader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,26 @@ open class VRMLoader {
return try VRM(data: data)
}

open func loadMeta(withURL url: URL) throws -> VRM0.Meta {
let data = try Data(contentsOf: url)
return try loadMeta(withData: data)
}

open func loadMeta(withData data: Data) throws -> VRM0.Meta {
let gltf = try BinaryGLTF(jsonDataOnly: data)
let rawExtensions = try gltf.jsonData.extensions ??? .keyNotFound("extensions")
let extensions = try rawExtensions.value as? [String: [String: Any]] ??? .dataInconsistent("extension type mismatch")
let decoder = DictionaryDecoder()

if let vrm1 = extensions["VRMC_vrm"] {
let meta = try decoder.decode(VRM1.Meta.self, from: try vrm1["meta"] ??? .keyNotFound("meta"))
return VRM0.Meta(vrm1: meta)
}

let vrm0 = try extensions["VRM"] ??? .keyNotFound("VRM")
return try decoder.decode(VRM0.Meta.self, from: try vrm0["meta"] ??? .keyNotFound("meta"))
}

open func loadThumbnail(from vrm: VRM) throws -> VRMImage {
guard let textureIndex = vrm.meta.texture, textureIndex >= 0 else {
throw VRMError.thumbnailNotFound
Expand Down
10 changes: 6 additions & 4 deletions Tests/VRMKitTests/Resources.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ enum Resources {
case seedSan

var data: Data {
return try! Data(contentsOf: url)
}

var url: URL {
switch self {
case .aliciaSolid:
let url = Bundle.module.url(forResource: "AliciaSolid", withExtension: "vrm")!
return try! Data(contentsOf: url)
return Bundle.module.url(forResource: "AliciaSolid", withExtension: "vrm")!
case .seedSan:
let url = Bundle.module.url(forResource: "Seed-san", withExtension: "vrm")!
return try! Data(contentsOf: url)
return Bundle.module.url(forResource: "Seed-san", withExtension: "vrm")!
}
}
}
30 changes: 30 additions & 0 deletions Tests/VRMKitTests/VRMLoaderTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import XCTest
import VRMKit

class VRMLoaderTests: XCTestCase {

func testLoadMetaFromVRM0Data() throws {
let meta = try VRMLoader().loadMeta(withData: Resources.aliciaSolid.data)

XCTAssertEqual(meta.title, "Alicia Solid")
XCTAssertEqual(meta.author, "DWANGO Co., Ltd.")
XCTAssertEqual(meta.texture, 6)
XCTAssertEqual(meta.licenseName, "Other")
}

func testLoadMetaFromVRM1Data() throws {
let meta = try VRMLoader().loadMeta(withData: Resources.seedSan.data)

XCTAssertEqual(meta.title, "Seed-san")
XCTAssertEqual(meta.author, "VirtualCast, Inc.")
XCTAssertEqual(meta.texture, 14)
XCTAssertEqual(meta.licenseName, "https://vrm.dev/licenses/1.0/")
}

func testLoadMetaFromURL() throws {
let meta = try VRMLoader().loadMeta(withURL: Resources.aliciaSolid.url)

XCTAssertEqual(meta.title, "Alicia Solid")
XCTAssertEqual(meta.author, "DWANGO Co., Ltd.")
}
}
Loading