Skip to content
Merged
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
4 changes: 2 additions & 2 deletions Sources/GDTF.swift
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ public struct FixtureAttribute: Codable {
}

public struct SubPhysicalUnit: Codable {
public var type: SubPhysicalType
public var type: SubPhysicalType?
public var physicalUnit: PhysicalUnit = .none
public var physicalFrom: Double = 0
public var physicalTo: Double = 1
Expand Down Expand Up @@ -280,7 +280,7 @@ public struct SubChannelSet: Codable {
public var name: String
public var physicalFrom: Double
public var physicalTo: Double
public var subPhysicalUnit: SubPhysicalUnit
public var subPhysicalUnit: SubPhysicalUnit?
public var wheelSlotIndex: Int?
public var dmxProfile: DMXProfile?
}
Expand Down
46 changes: 31 additions & 15 deletions Sources/Types.swift
Original file line number Diff line number Diff line change
Expand Up @@ -195,8 +195,11 @@ public extension DMXValue {
}

init(from rawValue: String) {
let split: [Int] = rawValue.split(separator: "/").map { Int($0) ?? 0 }
self.init(value: split[0], byteCount: split[1])
let parts = rawValue.split(separator: "/")
let value = parts.first.flatMap { Int($0) } ?? 0
// The byte-count component may carry an "s" suffix (byte-shifting form, e.g. "255/1s").
let byteCount = parts.count > 1 ? (Int(parts[1].filter(\.isNumber)) ?? 1) : 1
self.init(value: value, byteCount: byteCount)
}
}

Expand All @@ -213,13 +216,19 @@ public struct ColorCIE: Codable {
}

extension ColorCIE {
init(from rawValue: String) {
let split: [Double] = rawValue.split(separator: ",").map { Double($0) ?? 0 }

init(from rawValue: String) {
let split = rawValue.split(separator: ",").map { Double($0.trimmingCharacters(in: .whitespaces)) ?? 0 }

// "None" or a malformed value falls back to the white point rather than crashing.
guard split.count >= 2 else {
self = ColorCIE(x: 0.3127, y: 0.3290, Y: 1.0)
return
}

self.x = split[0]
self.y = split[1]
if (split.count == 3) {

if split.count >= 3 {
self.Y = split[2] > 1 ? split[2] / 100 : split[2]
} else {
self.Y = 1.0
Expand All @@ -232,20 +241,27 @@ public struct Rotation: Codable {
}

extension Rotation {
static let identity: [[Double]] = [[1, 0, 0], [0, 1, 0], [0, 0, 1]]

init(from rawValue: String) {
// "None" is a legal rotationtype value.
guard rawValue != "None" else {
self.matrix = Rotation.identity
return
}

var strMatrix = rawValue
strMatrix = strMatrix.replacingOccurrences(of: "}{", with: ",")
strMatrix = strMatrix.replacingOccurrences(of: "{", with: "")
strMatrix = strMatrix.replacingOccurrences(of: "}", with: "")

let flatMatrix: [Double] = strMatrix.split(separator: ",").map{ Double($0) ?? 0 }
assert(flatMatrix.count == 9)

/// convert 1D array into 3x3 2D array (matrix)
let matrix: [[Double]] = stride(from: 0, to: flatMatrix.count,by: 3)
.map{ Array(flatMatrix[$0..<$0 + 3]) }

self.matrix = matrix
let flatMatrix: [Double] = strMatrix.split(separator: ",").map { Double($0) ?? 0 }
guard flatMatrix.count == 9 else {
self.matrix = Rotation.identity
return
}

self.matrix = stride(from: 0, to: flatMatrix.count, by: 3).map { Array(flatMatrix[$0..<$0 + 3]) }
}
}

Expand Down
82 changes: 42 additions & 40 deletions Sources/XMLProcessor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ extension FixtureType: XMLDecodable {
guard let element = xml.element else { throw XMLParsingError.elementMissing }

self.name = try element.attribute(named: "Name").text
self.shortName = try element.attribute(named: "ShortName").text
self.longName = try element.attribute(named: "LongName").text
self.shortName = element.attribute(by: "ShortName")?.text ?? ""
self.longName = element.attribute(by: "LongName")?.text ?? ""
self.manufacturer = try element.attribute(named: "Manufacturer").text
self.description = try element.attribute(named: "Description").text
self.fixtureTypeID = try element.attribute(named: "FixtureTypeID").uuid
Expand All @@ -54,8 +54,8 @@ extension FixtureInfo: XMLDecodable {
guard let element = xml.element else { throw XMLParsingError.elementMissing }

self.name = try element.attribute(named: "Name").text
self.shortName = try element.attribute(named: "ShortName").text
self.longName = try element.attribute(named: "LongName").text
self.shortName = element.attribute(by: "ShortName")?.text ?? ""
self.longName = element.attribute(by: "LongName")?.text ?? ""
self.manufacturer = try element.attribute(named: "Manufacturer").text
self.description = try element.attribute(named: "Description").text
self.fixtureTypeID = try element.attribute(named: "FixtureTypeID").text
Expand Down Expand Up @@ -142,8 +142,8 @@ extension SubPhysicalUnit: XMLDecodable {
self.physicalTo = element.attribute(by: "PhysicalTo")?.double ?? 1

self.physicalUnit = (try? element.attribute(by: "PhysicalUnit")?.toEnum()) ?? .none
self.type = try element.attribute(named: "Type").toEnum()

self.type = try? element.attribute(by: "Type")?.toEnum()
}
}

Expand All @@ -165,8 +165,8 @@ extension Slot: XMLDecodableWithIndex {
guard let element = xml.element else { throw XMLParsingError.elementMissing }

self.name = try element.attribute(named: "Name").text
self.color = try ColorCIE(from: element.attribute(named: "Color").text)
self.color = element.attribute(by: "Color").map { ColorCIE(from: $0.text) } ?? ColorCIE(x: 0.3127, y: 0.3290, Y: 1.0)

self.filter = try element.attribute(by: "Filter")?.resolveNode(base: tree["PhysicalDescriptions"]["Filters"], tree: tree)

self.mediaFileName = FileResource(name: element.attribute(by: "MediaFileName")?.text, fileExtension: "png")
Expand Down Expand Up @@ -223,8 +223,8 @@ extension Emitter: XMLDecodable {
guard let element = xml.element else { throw XMLParsingError.elementMissing }
self.name = try element.attribute(named: "Name").text

self.color = try ColorCIE(from: element.attribute(named: "Color").text)
self.dominantWavelength = try element.attribute(named: "DominantWaveLength").double
self.color = element.attribute(by: "Color").map { ColorCIE(from: $0.text) }
self.dominantWavelength = element.attribute(by: "DominantWaveLength")?.double


self.diodePart = element.attribute(by: "DiodePart")?.text
Expand Down Expand Up @@ -264,8 +264,8 @@ extension Filter: XMLDecodable {
guard let element = xml.element else { throw XMLParsingError.elementMissing }

self.name = try element.attribute(named: "Name").text
self.color = try ColorCIE(from: element.attribute(named: "Color").text)
self.color = element.attribute(by: "Color").map { ColorCIE(from: $0.text) } ?? ColorCIE(x: 0.3127, y: 0.3290, Y: 1.0)

self.measurements = try xml.parseChildrenToArray(tree: tree)
}
}
Expand All @@ -275,15 +275,21 @@ extension ColorSpace: XMLDecodable {
guard let element = xml.element else { throw XMLParsingError.elementMissing }

self.name = element.attribute(by: "Name")?.text ?? "Default"
self.mode = try element.attribute(named: "Mode").toEnum()
self.mode = (try? element.attribute(by: "Mode")?.toEnum()) ?? .srgb

// Custom-gamut primaries (used when Mode = Custom).
self.red = element.attribute(by: "Red").map { ColorCIE(from: $0.text) }
self.green = element.attribute(by: "Green").map { ColorCIE(from: $0.text) }
self.blue = element.attribute(by: "Blue").map { ColorCIE(from: $0.text) }
self.whitePoint = element.attribute(by: "WhitePoint").map { ColorCIE(from: $0.text) }
}
}

extension DMXProfile: XMLDecodable {
init(xml: XMLIndexer, tree: XMLIndexer) throws {
guard let element = xml.element else { throw XMLParsingError.elementMissing }

self.name = try element.attribute(named: "Name").text
self.name = element.attribute(by: "Name")?.text ?? ""
self.points = try xml.parseChildrenToArray(tree: tree)
}
}
Expand All @@ -292,12 +298,12 @@ extension Point: XMLDecodable {
init(xml: XMLIndexer, tree: XMLIndexer) throws {
guard let element = xml.element else { throw XMLParsingError.elementMissing }

self.dmxPercentage = try Double(element.attribute(named: "DMXPercentage").text) ?? 0
self.cfc0 = try element.attribute(named: "CFC0").double ?? 0
self.cfc1 = try element.attribute(named: "CFC1").double ?? 0
self.cfc2 = try element.attribute(named: "CFC2").double ?? 0
self.cfc3 = try element.attribute(named: "CFC3").double ?? 0
self.dmxPercentage = element.attribute(by: "DMXPercentage")?.double ?? 0

self.cfc0 = element.attribute(by: "CFC0")?.double ?? 0
self.cfc1 = element.attribute(by: "CFC1")?.double ?? 0
self.cfc2 = element.attribute(by: "CFC2")?.double ?? 0
self.cfc3 = element.attribute(by: "CFC3")?.double ?? 0
}
}

Expand Down Expand Up @@ -348,14 +354,10 @@ extension DMXChannel: XMLDecodable {

// TODO: Handle overrides from geometry nodes

if let dmxBreak = element.attribute(by: "DMXBreak") {
self.dmxBreak = dmxBreak.int ?? 0

if dmxBreak.text != "None" {
self.offset = dmxBreak.text.split(separator: ",").map { Int($0) ?? 0 }
}
} else {
self.dmxBreak = 0
self.dmxBreak = element.attribute(by: "DMXBreak")?.int ?? 1

if let offset = element.attribute(by: "Offset"), offset.text != "None" {
self.offset = offset.text.split(separator: ",").map { Int($0) ?? 0 }
}


Expand Down Expand Up @@ -409,7 +411,7 @@ extension LogicalChannel: XMLDecodable {
self.snap = (try? element.attribute(by: "Snap")?.toEnum()) ?? .no
self.master = (try? element.attribute(by: "Master")?.toEnum()) ?? .none

self.mibFade = element.attribute(by: "MIBFade")?.double ?? 0
self.mibFade = (element.attribute(by: "MibFade") ?? element.attribute(by: "MIBFade"))?.double ?? 0
self.dmxChangeTimeLimit = element.attribute(by: "DMXChangeTimeLimit")?.double ?? 0

self.channelFunctions = try xml.parseChildrenToArray(tree: tree)
Expand Down Expand Up @@ -446,8 +448,8 @@ extension ChannelFunction: XMLDecodableWithIndex {
// Filter
self.filter = try element.attribute(by: "Filter")?.resolveNode(base: tree["PhysicalDescriptions"]["Filters"], tree: tree)

// ColorSpace
self.colorSpace = try element.attribute(by: "ColorSpace")?.resolveNode(base: tree["PhysicalDescriptions"]["AdditionalColorSpaces"], tree: tree)
// ColorSpace (default child or an AdditionalColorSpaces entry)
self.colorSpace = try? element.attribute(by: "ColorSpace")?.resolveNode(base: tree["PhysicalDescriptions"], tree: tree)

// Mode Master
self.modeMaster = element.attribute(by: "ModeMaster")?.text
Expand All @@ -458,7 +460,7 @@ extension ChannelFunction: XMLDecodableWithIndex {
}

// DMX Profile
self.dmxProfile = try? element.attribute(by: "DMXProfile")?.resolveNode(base: tree["DMXProfiles"], tree: tree)
self.dmxProfile = try? element.attribute(by: "DMXProfile")?.resolveNode(base: tree["PhysicalDescriptions"]["DMXProfiles"], tree: tree)

self.minimum = element.attribute(by: "Min")?.double ?? self.physicalFrom
self.maximum = element.attribute(by: "Max")?.double ?? self.physicalTo
Expand Down Expand Up @@ -498,16 +500,16 @@ extension SubChannelSet: XMLDecodableWithParent {
self.physicalFrom = (try? element.attribute(named: "PhysicalFrom"))?.double ?? 0
self.physicalTo = (try? element.attribute(named: "PhysicalTo"))?.double ?? 1

// needs the parent
guard let attributeName = try parent.element?.attribute(named: "Attribute") else {
throw XMLParsingError.attributeMissing(named: "Attribute", on: parent.element)
// SubPhysicalUnit is optional; resolve it against the parent function's attribute.
if let subPhysical = element.attribute(by: "SubPhysicalUnit"),
let attributeName = parent.element?.attribute(by: "Attribute") {
let associatedAttribute = try tree["AttributeDefinitions"]["Attributes"].findChild(with: "Name", being: attributeName.text)
self.subPhysicalUnit = try subPhysical.resolveNode(base: associatedAttribute, tree: tree)
} else {
self.subPhysicalUnit = nil
}

let associatedAttribute = try tree["AttributeDefinitions"]["Attributes"].findChild(with: "Name", being: attributeName.text)

self.subPhysicalUnit = try element.attribute(named: "SubPhysicalUnit").resolveNode(base: associatedAttribute, tree: tree)

self.dmxProfile = try? element.attribute(named: "DMXProfile").resolveNode(base: tree["DMXProfiles"], tree: tree)
self.dmxProfile = try? element.attribute(by: "DMXProfile")?.resolveNode(base: tree["PhysicalDescriptions"]["DMXProfiles"], tree: tree)
}
}

Expand Down
79 changes: 79 additions & 0 deletions Tests/SwiftGDTFTests/SpecLegalInputsTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import XCTest
@testable import SwiftGDTF

/// Tests covering spec-legal inputs
final class SpecLegalInputsTests: XCTestCase {

// MARK: DMXValue (dmxtype: "value/byteCount" with optional trailing "s")

func testDMXValueBasic() {
let v = DMXValue(from: "255/1")
XCTAssertEqual(v.value, 255)
XCTAssertEqual(v.byteCount, 1)
XCTAssertEqual(v.bytes, [255])
}

func testDMXValueSixteenBit() {
let v = DMXValue(from: "65535/2")
XCTAssertEqual(v.byteCount, 2)
XCTAssertEqual(v.bytes, [255, 255])
}

func testDMXValueByteShiftingSuffix() {
// "255/1s" is the byte-shifting form; the "s" must not corrupt byteCount.
let v = DMXValue(from: "255/1s")
XCTAssertEqual(v.value, 255)
XCTAssertEqual(v.byteCount, 1)
XCTAssertEqual(v.bytes, [255])
}

func testDMXValueMissingByteCountDoesNotCrash() {
let v = DMXValue(from: "5")
XCTAssertEqual(v.value, 5)
XCTAssertEqual(v.byteCount, 1)
}

// MARK: ColorCIE (vector3type: "x,y,Y"; "None" is legal)

func testColorCIEFull() {
let c = ColorCIE(from: "0.3127,0.3290,100")
XCTAssertEqual(c.x, 0.3127, accuracy: 1e-6)
XCTAssertEqual(c.y, 0.3290, accuracy: 1e-6)
XCTAssertEqual(c.Y, 1.0, accuracy: 1e-6) // Y > 1 is normalized /100
}

func testColorCIETwoComponents() {
let c = ColorCIE(from: "0.5,0.5")
XCTAssertEqual(c.x, 0.5, accuracy: 1e-6)
XCTAssertEqual(c.Y, 1.0, accuracy: 1e-6)
}

func testColorCIENoneFallsBackToWhite() {
let c = ColorCIE(from: "None")
XCTAssertEqual(c.x, 0.3127, accuracy: 1e-6)
XCTAssertEqual(c.y, 0.3290, accuracy: 1e-6)
}

func testColorCIEWhitespaceTolerated() {
let c = ColorCIE(from: "0.5, 0.5, 50")
XCTAssertEqual(c.x, 0.5, accuracy: 1e-6)
XCTAssertEqual(c.y, 0.5, accuracy: 1e-6)
}

// MARK: Rotation (rotationtype: "{..}{..}{..}"; "None" is legal)

func testRotationMatrix() {
let r = Rotation(from: "{1,0,0}{0,1,0}{0,0,1}")
XCTAssertEqual(r.matrix, [[1, 0, 0], [0, 1, 0], [0, 0, 1]])
}

func testRotationNoneIsIdentity() {
let r = Rotation(from: "None")
XCTAssertEqual(r.matrix, [[1, 0, 0], [0, 1, 0], [0, 0, 1]])
}

func testRotationMalformedDoesNotCrash() {
let r = Rotation(from: "{1,2}")
XCTAssertEqual(r.matrix, [[1, 0, 0], [0, 1, 0], [0, 0, 1]])
}
}
Loading