github.com/boki/go-xmp@v1.0.1/models/xmp_dm/types.go (about) 1 // Copyright (c) 2017-2018 Alexander Eichhorn 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"): you may 4 // not use this file except in compliance with the License. You may obtain 5 // a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 // License for the specific language governing permissions and limitations 13 // under the License. 14 15 // Package xmpdm implements the XMP Dynamic Media namespace as defined by XMP Specification Part 2. 16 package xmpdm 17 18 import ( 19 "bytes" 20 "encoding/json" 21 "fmt" 22 "math" 23 "sort" 24 "strconv" 25 "strings" 26 "time" 27 "trimmer.io/go-xmp/xmp" 28 ) 29 30 // 1.2.3.1 Part 31 // [/]time:## - duration from node=0 32 // [/]time:##d## - duration from given node 33 // [/]time:##r## - range from given node to end 34 // 35 // ## is FrameCount 36 type Part struct { 37 Path string 38 Start FrameCount 39 Duration FrameCount 40 } 41 42 func prefixIfNot(s, p string) string { 43 if strings.HasPrefix(s, p) { 44 return s 45 } 46 return p + s 47 } 48 49 func NewPart(v string) Part { 50 return Part{ 51 Path: prefixIfNot(v, "/"), 52 } 53 } 54 55 func NewPartList(s ...string) PartList { 56 l := make(PartList, len(s)) 57 for i, v := range s { 58 l[i].Path = prefixIfNot(v, "/") 59 } 60 return l 61 } 62 63 func (x Part) IsZero() bool { 64 return x.Path == "" && x.Start.IsZero() && x.Duration.IsZero() 65 } 66 67 func (x Part) MarshalXMP(e *xmp.Encoder, node *xmp.Node, m xmp.Model) error { 68 if x.IsZero() { 69 return nil 70 } 71 type _t Part 72 return e.EncodeElement(_t(x), node) 73 } 74 75 func (x *Part) UnmarshalText(data []byte) error { 76 p := Part{} 77 v := string(data) 78 tokens := strings.Split(v, "time:") 79 p.Path = tokens[0] 80 if len(tokens) > 1 { 81 t := tokens[1] 82 if i := strings.Index(t, "d"); i > -1 { 83 // node + duration 84 if err := p.Start.UnmarshalText([]byte(t[:i])); err != nil { 85 return fmt.Errorf("xmp: invalid part node: %v", err) 86 } 87 if err := p.Duration.UnmarshalText([]byte(t[i+1:])); err != nil { 88 return fmt.Errorf("xmp: invalid part duration: %v", err) 89 } 90 } else if i = strings.Index(t, "r"); i > -1 { 91 // node + end range 92 if err := p.Start.UnmarshalText([]byte(t[:i])); err != nil { 93 return fmt.Errorf("xmp: invalid part range node: %v", err) 94 } 95 var end FrameCount 96 if err := end.UnmarshalText([]byte(t[i+1:])); err != nil { 97 return fmt.Errorf("xmp: invalid part range end: %v", err) 98 } 99 p.Duration = end.Sub(p.Start) 100 } else { 101 // duration with zero node 102 if err := p.Duration.UnmarshalText([]byte(t)); err != nil { 103 return fmt.Errorf("xmp: invalid part duration: %v", err) 104 } 105 } 106 } 107 *x = p 108 return nil 109 } 110 111 func (x Part) MarshalText() ([]byte, error) { 112 buf := bytes.Buffer{} 113 buf.WriteString(x.Path) 114 if len(x.Path) > 0 && !strings.HasSuffix(x.Path, "/") { 115 buf.WriteByte('/') 116 } 117 if x.Duration.IsZero() { 118 return buf.Bytes(), nil 119 } 120 buf.WriteString("time:") 121 if !x.Start.IsZero() { 122 buf.WriteString(x.Start.String()) 123 buf.WriteByte('d') 124 } 125 buf.WriteString(x.Duration.String()) 126 return buf.Bytes(), nil 127 } 128 129 type PartList []Part 130 131 func (x PartList) String() string { 132 if b, err := x.MarshalText(); err == nil { 133 return string(b) 134 } 135 return "" 136 } 137 138 func (x *PartList) Add(v string) { 139 *x = append(*x, Part{Path: v}) 140 } 141 142 func (x *PartList) AddPart(p Part) { 143 *x = append(*x, p) 144 } 145 146 func (x *PartList) UnmarshalText(data []byte) error { 147 l := make(PartList, 0) 148 for _, v := range bytes.Split(data, []byte(";")) { 149 p := Part{} 150 if err := p.UnmarshalText(v); err != nil { 151 return err 152 } 153 l = append(l, p) 154 } 155 *x = l 156 return nil 157 } 158 159 func (x PartList) MarshalText() ([]byte, error) { 160 s := make([][]byte, len(x)) 161 for i, v := range x { 162 b, _ := v.MarshalText() 163 if !bytes.HasPrefix(b, []byte("/")) { 164 b = append([]byte("/"), b...) 165 } 166 s[i] = b 167 } 168 return bytes.Join(s, []byte(";")), nil 169 } 170 171 // 1.2.4.1 ResourceRef - stRef:maskMarkers closed choice 172 type MaskType string 173 174 const ( 175 MaskAll MaskType = "All" 176 MaskNone MaskType = "None" 177 ) 178 179 // 1.2.6.3 FrameCount 180 // ##<FrameRate> 181 type FrameCount struct { 182 Count int64 183 Rate FrameRate 184 } 185 186 func (x FrameCount) IsZero() bool { 187 return x.Count == 0 && x.Rate.IsZero() 188 } 189 190 func (x FrameCount) Sub(y FrameCount) FrameCount { 191 return FrameCount{ 192 Count: x.Count - y.Count, 193 Rate: x.Rate, 194 } 195 } 196 197 func (x FrameCount) IsSmaller(y FrameCount) bool { 198 return float64(x.Count)*x.Rate.Value() < float64(y.Count)*y.Rate.Value() 199 } 200 201 func (x *FrameCount) UnmarshalText(data []byte) error { 202 var err error 203 c := FrameCount{ 204 Count: 0, 205 Rate: FrameRate{1, 1}, 206 } 207 v := string(data) 208 if fpos := strings.Index(v, "f"); fpos > -1 { 209 // ##f##s## format 210 if err := c.Rate.UnmarshalText([]byte(v[fpos:])); err != nil { 211 return fmt.Errorf("xmp: invalid frame count '%s': %v", v, err) 212 } 213 v = v[:fpos] 214 } 215 // special "maximum" 216 if v == "maximum" { 217 c.Count = -1 218 } else { 219 // ## format 220 if c.Count, err = strconv.ParseInt(v, 10, 64); err != nil { 221 return fmt.Errorf("xmp: invalid frame counter value '%s': %v", v, err) 222 } 223 } 224 *x = c 225 return nil 226 } 227 228 func (x FrameCount) String() string { 229 switch x.Count { 230 case 0: 231 return "0" 232 case -1: 233 return "maximum" 234 default: 235 buf := bytes.Buffer{} 236 buf.WriteString(strconv.FormatInt(x.Count, 10)) 237 buf.WriteString(x.Rate.String()) 238 return buf.String() 239 } 240 } 241 242 func (x FrameCount) MarshalText() ([]byte, error) { 243 return []byte(x.String()), nil 244 } 245 246 // 1.2.6.1 beatSpliceStretch 247 type BeatSpliceStretch struct { 248 RiseInDecibel float64 `xmp:"xmpDM:riseInDecibel,attr"` 249 RiseInTimeDuration MediaTime `xmp:"xmpDM:riseInTimeDuration"` 250 UseFileBeatsMarker xmp.Bool `xmp:"xmpDM:useFileBeatsMarker,attr"` 251 } 252 253 func (x BeatSpliceStretch) IsZero() bool { 254 return x.RiseInDecibel == 0 && x.RiseInTimeDuration.IsZero() && !x.UseFileBeatsMarker.Value() 255 } 256 257 // 1.2.6.2 CuePointParam 258 type CuePointParam struct { 259 Key string `xmp:"xmpDM:key,attr"` 260 Value string `xmp:"xmpDM:value,attr"` 261 } 262 263 func (x CuePointParam) IsZero() bool { 264 return x.Value == "" && x.Key == "" 265 } 266 267 type CuePointParamArray []CuePointParam 268 269 func (x CuePointParamArray) Typ() xmp.ArrayType { 270 return xmp.ArrayTypeOrdered 271 } 272 273 func (x CuePointParamArray) MarshalXMP(e *xmp.Encoder, node *xmp.Node, m xmp.Model) error { 274 return xmp.MarshalArray(e, node, x.Typ(), x) 275 } 276 277 func (x *CuePointParamArray) UnmarshalXMP(d *xmp.Decoder, node *xmp.Node, m xmp.Model) error { 278 return xmp.UnmarshalArray(d, node, x.Typ(), x) 279 } 280 281 type VideoFrameRate float64 282 283 func (x *VideoFrameRate) UnmarshalText(data []byte) error { 284 // try detecting supported strings 285 switch strings.ToUpper(string(data)) { 286 case "NTSC": 287 *x = VideoFrameRate(29.97) 288 return nil 289 case "PAL": 290 *x = VideoFrameRate(25) 291 return nil 292 } 293 294 // try parsing as rational 295 rat := &xmp.Rational{} 296 if err := rat.UnmarshalText(data); err == nil { 297 *x = VideoFrameRate(rat.Value()) 298 return nil 299 } 300 301 // parse as float value, strip trailing "fps" 302 v := strings.TrimSpace(strings.TrimSuffix(string(data), "fps")) 303 v = strings.TrimSuffix(v, "i") 304 v = strings.TrimSuffix(v, "p") 305 if f, err := strconv.ParseFloat(v, 64); err != nil { 306 return fmt.Errorf("xmp: invalid video frame rate value '%s': %v", v, err) 307 } else { 308 *x = VideoFrameRate(f) 309 } 310 return nil 311 } 312 313 // 1.2.6.4 FrameRate 314 // "f###" 315 // "f###s###" 316 type FrameRate struct { 317 Rate int64 318 Base int64 319 } 320 321 func (x FrameRate) Value() float64 { 322 if x.Base == 0 { 323 return 0 324 } 325 return float64(x.Rate) / float64(x.Base) 326 } 327 328 func (x FrameRate) String() string { 329 buf := bytes.Buffer{} 330 buf.WriteByte('f') 331 buf.WriteString(strconv.FormatInt(x.Rate, 10)) 332 if x.Base > 1 { 333 buf.WriteByte('s') 334 buf.WriteString(strconv.FormatInt(x.Base, 10)) 335 } 336 return buf.String() 337 } 338 339 func (x FrameRate) IsZero() bool { 340 return x.Base == 0 || x.Rate == 0 341 } 342 343 func (x *FrameRate) UnmarshalText(data []byte) error { 344 if !bytes.HasPrefix(data, []byte("f")) { 345 return fmt.Errorf("xmp: invalid frame rate value '%s'", string(data)) 346 } 347 v := data[1:] 348 r := FrameRate{} 349 tokens := bytes.Split(v, []byte("s")) 350 var err error 351 r.Rate, err = strconv.ParseInt(string(tokens[0]), 10, 64) 352 if err != nil { 353 return fmt.Errorf("xmp: invalid frame rate value '%s': %v", string(data), err) 354 } 355 if len(tokens) > 1 { 356 r.Base, err = strconv.ParseInt(string(tokens[1]), 10, 64) 357 if err != nil { 358 return fmt.Errorf("xmp: invalid frame rate value '%s': %v", string(data), err) 359 } 360 } 361 if r.Base == 0 { 362 r.Base = 1 363 } 364 *x = r 365 return nil 366 } 367 368 func (x FrameRate) MarshalText() ([]byte, error) { 369 return []byte(x.String()), nil 370 } 371 372 // 1.2.6.5 Marker 373 type Marker struct { 374 Comment string `xmp:"xmpDM:comment,attr"` 375 CuePointParams CuePointParamArray `xmp:"xmpDM:cuePointParams"` 376 CuePointType string `xmp:"xmpDM:cuePointType,attr"` 377 Duration FrameCount `xmp:"xmpDM:duration,attr"` 378 Location xmp.Uri `xmp:"xmpDM:location"` 379 Name string `xmp:"xmpDM:name,attr"` 380 Probability float64 `xmp:"xmpDM:probability,attr"` 381 Speaker string `xmp:"xmpDM:speaker,attr"` 382 StartTime FrameCount `xmp:"xmpDM:startTime,attr"` 383 Target string `xmp:"xmpDM:target,attr"` 384 Types MarkerTypeList `xmp:"xmpDM:type"` 385 } 386 387 func (x Marker) IsZero() bool { 388 return x.Comment == "" && 389 len(x.CuePointParams) == 0 && 390 x.CuePointType == "" && 391 x.Duration.IsZero() && 392 x.Location.IsZero() && 393 x.Name == "" && 394 x.Probability == 0 && 395 x.Speaker == "" && 396 x.StartTime.IsZero() && 397 x.Target == "" && 398 len(x.Types) == 0 399 } 400 401 type MarkerList []Marker 402 403 func (x MarkerList) Typ() xmp.ArrayType { 404 return xmp.ArrayTypeOrdered 405 } 406 407 func (x MarkerList) MarshalXMP(e *xmp.Encoder, node *xmp.Node, m xmp.Model) error { 408 return xmp.MarshalArray(e, node, x.Typ(), x) 409 } 410 411 func (x *MarkerList) UnmarshalXMP(d *xmp.Decoder, node *xmp.Node, m xmp.Model) error { 412 return xmp.UnmarshalArray(d, node, x.Typ(), x) 413 } 414 415 func (x MarkerList) Len() int { return len(x) } 416 func (x MarkerList) Swap(i, j int) { x[i], x[j] = x[j], x[i] } 417 func (x MarkerList) Less(i, j int) bool { return x[i].StartTime.IsSmaller(x[j].StartTime) } 418 419 func (x *MarkerList) Sort() { 420 sort.Sort(x) 421 } 422 423 func (x MarkerList) Filter(types MarkerTypeList) MarkerList { 424 l := make(MarkerList, 0) 425 for _, v := range x { 426 if len(types.Intersect(v.Types)) > 0 { 427 l = append(l, v) 428 } 429 } 430 return l 431 } 432 433 type MarkerTypeList []MarkerType 434 435 func (x MarkerTypeList) Intersect(y MarkerTypeList) MarkerTypeList { 436 m := make(map[MarkerType]bool) 437 for _, v := range x { 438 m[v] = true 439 } 440 r := make(MarkerTypeList, 0) 441 for _, v := range y { 442 if _, ok := m[v]; ok { 443 r = append(r, v) 444 } 445 } 446 return r 447 } 448 449 func (x MarkerTypeList) Contains(t MarkerType) bool { 450 for _, v := range x { 451 if v == t { 452 return true 453 } 454 } 455 return false 456 } 457 458 // 1.2.6.6 Media 459 type Media struct { 460 Duration MediaTime `xmp:"xmpDM:duration"` 461 Managed xmp.Bool `xmp:"xmpDM:managed,attr"` 462 Path xmp.Uri `xmp:"xmpDM:path,attr"` 463 StartTime MediaTime `xmp:"xmpDM:startTime"` 464 Track string `xmp:"xmpDM:track,attr"` 465 WebStatement xmp.Uri `xmp:"xmpDM:webStatement"` 466 } 467 468 type MediaArray []Media 469 470 func (x MediaArray) Typ() xmp.ArrayType { 471 return xmp.ArrayTypeUnordered 472 } 473 474 func (x MediaArray) MarshalXMP(e *xmp.Encoder, node *xmp.Node, m xmp.Model) error { 475 return xmp.MarshalArray(e, node, x.Typ(), x) 476 } 477 478 func (x *MediaArray) UnmarshalXMP(d *xmp.Decoder, node *xmp.Node, m xmp.Model) error { 479 return xmp.UnmarshalArray(d, node, x.Typ(), x) 480 } 481 482 // 1.2.6.7 ProjectLink 483 type ProjectLink struct { 484 Path xmp.Uri `xmp:"xmpDM:path"` 485 Type FileType `xmp:"xmpDM:type,attr"` 486 } 487 488 func (x ProjectLink) IsZero() bool { 489 return x.Path == "" && x.Type == "" 490 } 491 492 // 1.2.6.8 resampleStretch 493 type ResampleStretch struct { 494 Quality Quality `xmp:"xmpDM:quality,attr"` 495 } 496 497 func (x ResampleStretch) IsZero() bool { 498 return x.Quality == "" 499 } 500 501 // 1.2.6.9 Time 502 type MediaTime struct { 503 Scale xmp.Rational `xmp:"xmpDM:scale,attr"` 504 Value int64 `xmp:"xmpDM:value,attr"` 505 } 506 507 func (x MediaTime) IsZero() bool { 508 return x.Value == 0 && x.Scale.Den == 0 509 } 510 511 // Parse when used as attribute (not standard-conform) 512 // xmpDM:duration="0:07.680" 513 func (x *MediaTime) UnmarshalXMPAttr(d *xmp.Decoder, a xmp.Attr) error { 514 v := string(a.Value) 515 var dur time.Duration 516 fields := strings.Split(v, ".") 517 if len(fields) > 1 { 518 if i, err := strconv.ParseInt(fields[1], 10, 64); err == nil { 519 dur = time.Duration(i * int64(math.Pow10(9-len(fields[1])))) 520 } 521 } 522 fields = strings.Split(fields[0], ":") 523 var h, m, s int 524 switch len(fields) { 525 case 3: 526 h, _ = strconv.Atoi(fields[0]) 527 m, _ = strconv.Atoi(fields[1]) 528 s, _ = strconv.Atoi(fields[2]) 529 case 2: 530 m, _ = strconv.Atoi(fields[0]) 531 s, _ = strconv.Atoi(fields[1]) 532 case 1: 533 s, _ = strconv.Atoi(fields[0]) 534 } 535 dur += time.Hour*time.Duration(h) + time.Minute*time.Duration(m) + time.Second*time.Duration(s) 536 *x = MediaTime{ 537 Value: int64(dur / time.Millisecond), 538 Scale: xmp.Rational{ 539 Num: 1, 540 Den: 1000, 541 }, 542 } 543 return nil 544 } 545 546 // Part 2: 1.2.6.10 Timecode 547 548 func ParseTimecodeFormat(f float64, isdrop bool) TimecodeFormat { 549 switch { 550 case 23.975 <= f && f < 23.997: 551 return TimecodeFormat23976 552 case f == 24: 553 return TimecodeFormat24 554 case f == 25: 555 return TimecodeFormat25 556 case 29.96 < f && f < 29.98: 557 if isdrop { 558 return TimecodeFormat2997 559 } else { 560 return TimecodeFormat2997ND 561 } 562 case f == 30: 563 return TimecodeFormat30 564 case f == 50: 565 return TimecodeFormat50 566 case 59.93 < f && f < 59.95: 567 if isdrop { 568 return TimecodeFormat5994 569 } else { 570 return TimecodeFormat5994 571 } 572 case f == 60: 573 return TimecodeFormat60 574 default: 575 return TimecodeFormatInvalid 576 } 577 } 578 579 type Timecode struct { 580 Format TimecodeFormat `xmp:"xmpDM:timeFormat"` 581 Value string `xmp:"xmpDM:timeValue"` // hh:mm:ss:ff or hh;mm;ss;ff 582 H int `xmp:"-"` 583 M int `xmp:"-"` 584 S int `xmp:"-"` 585 F int `xmp:"-"` 586 IsDropFrame bool `xmp:"-"` 587 } 588 589 func (x Timecode) String() string { 590 sep := ':' 591 if x.IsDropFrame { 592 sep = ';' 593 } 594 buf := bytes.Buffer{} 595 if x.H < 10 { 596 buf.WriteByte('0') 597 } 598 buf.WriteString(strconv.FormatInt(int64(x.H), 10)) 599 buf.WriteRune(sep) 600 if x.M < 10 { 601 buf.WriteByte('0') 602 } 603 buf.WriteString(strconv.FormatInt(int64(x.M), 10)) 604 buf.WriteRune(sep) 605 if x.S < 10 { 606 buf.WriteByte('0') 607 } 608 buf.WriteString(strconv.FormatInt(int64(x.S), 10)) 609 buf.WriteRune(sep) 610 if x.F < 10 { 611 buf.WriteByte('0') 612 } 613 buf.WriteString(strconv.FormatInt(int64(x.F), 10)) 614 return buf.String() 615 } 616 617 func (x *Timecode) Unpack() error { 618 sep := ":" 619 if x.IsDropFrame { 620 sep = ";" 621 } 622 fields := strings.Split(x.Value, sep) 623 if len(fields) < 4 { 624 return fmt.Errorf("found only %d of 4 fields", len(fields)) 625 } 626 var err error 627 if x.H, err = strconv.Atoi(fields[0]); err != nil { 628 return err 629 } 630 if x.M, err = strconv.Atoi(fields[1]); err != nil { 631 return err 632 } 633 if x.S, err = strconv.Atoi(fields[2]); err != nil { 634 return err 635 } 636 if x.F, err = strconv.Atoi(fields[3]); err != nil { 637 return err 638 } 639 return nil 640 } 641 642 func (x Timecode) IsZero() bool { 643 return x.Value == "" && x.Format == "" 644 } 645 646 func (x Timecode) MarshalJSON() ([]byte, error) { 647 x.Value = x.String() 648 type _t Timecode 649 return json.Marshal(_t(x)) 650 } 651 652 func (x *Timecode) UnmarshalJSON(data []byte) error { 653 xt := struct { 654 Format TimecodeFormat 655 Value string 656 }{} 657 if err := json.Unmarshal(data, &xt); err != nil { 658 return fmt.Errorf("xmp: invalid timecode: %v", err) 659 } 660 if len(xt.Value) == 0 { 661 xt.Value = "00:00:00:00" 662 } 663 tc := Timecode{ 664 Value: xt.Value, 665 Format: xt.Format, 666 IsDropFrame: strings.Contains(xt.Value, ";"), 667 } 668 if err := tc.Unpack(); err != nil { 669 return fmt.Errorf("xmp: invalid timecode '%s': %v", tc.Value, err) 670 } 671 *x = tc 672 return nil 673 } 674 675 func (x Timecode) MarshalXMP(e *xmp.Encoder, node *xmp.Node, m xmp.Model) error { 676 x.Value = x.String() 677 type _t Timecode 678 return e.EncodeElement(_t(x), node) 679 } 680 681 func (x *Timecode) UnmarshalXMP(d *xmp.Decoder, node *xmp.Node, m xmp.Model) error { 682 // Note: to avoid recursion we unmarshal into a temporary struct type 683 // and copy over the result 684 685 // support both the attribute form and the element form 686 var tc Timecode 687 xt := struct { 688 Format TimecodeFormat `xmp:"xmpDM:timeFormat"` 689 Value string `xmp:"xmpDM:timeValue"` 690 }{} 691 if err := d.DecodeElement(&xt, node); err != nil { 692 return fmt.Errorf("xmp: invalid timecode '%s': %v", node.Value, err) 693 } 694 if len(xt.Value) == 0 { 695 xt.Value = "00:00:00:00" 696 } 697 tc = Timecode{ 698 Value: xt.Value, 699 Format: xt.Format, 700 IsDropFrame: strings.Contains(xt.Value, ";"), 701 } 702 if err := tc.Unpack(); err != nil { 703 return fmt.Errorf("xmp: invalid timecode '%s': %v", tc.Value, err) 704 } 705 *x = tc 706 return nil 707 } 708 709 // 1.2.6.11 timeScaleStretch 710 type TimeScaleStretch struct { 711 Overlap float64 `xmp:"xmpDM:frameOverlappingPercentage,attr"` 712 FrameSize float64 `xmp:"xmpDM:frameSize,attr"` 713 Quality Quality `xmp:"xmpDM:quality,attr"` 714 } 715 716 func (x TimeScaleStretch) IsZero() bool { 717 return x.Overlap == 0 && x.FrameSize == 0 && x.Quality == "" 718 } 719 720 // 1.2.6.12 Track 721 type Track struct { 722 FrameRate FrameRate `xmp:"xmpDM:frameRate,attr"` 723 Markers MarkerList `xmp:"xmpDM:markers"` 724 Name string `xmp:"xmpDM:trackName,attr"` 725 Type MarkerTypeList `xmp:"xmpDM:trackType,attr"` 726 } 727 728 type TrackArray []Track 729 730 func (x TrackArray) Typ() xmp.ArrayType { 731 return xmp.ArrayTypeUnordered 732 } 733 734 func (x TrackArray) MarshalXMP(e *xmp.Encoder, node *xmp.Node, m xmp.Model) error { 735 return xmp.MarshalArray(e, node, x.Typ(), x) 736 } 737 738 func (x *TrackArray) UnmarshalXMP(d *xmp.Decoder, node *xmp.Node, m xmp.Model) error { 739 return xmp.UnmarshalArray(d, node, x.Typ(), x) 740 }