11// Copyright (c) Six Labors.
22// Licensed under the Six Labors Split License.
33
4- using System . Diagnostics ;
54using System . Text ;
5+ using System . Xml ;
66using System . Xml . Linq ;
77
88namespace SixLabors . ImageSharp . Metadata . Profiles . Xmp ;
@@ -25,18 +25,17 @@ public XmpProfile()
2525 /// Initializes a new instance of the <see cref="XmpProfile"/> class.
2626 /// </summary>
2727 /// <param name="data">The UTF8 encoded byte array to read the XMP profile from.</param>
28- public XmpProfile ( byte [ ] ? data ) => this . Data = data ;
28+ public XmpProfile ( byte [ ] ? data ) => this . Data = NormalizeDataIfNeeded ( data ) ;
2929
3030 /// <summary>
31- /// Initializes a new instance of the <see cref="XmpProfile"/> class
32- /// by making a copy from another XMP profile .
31+ /// Initializes a new instance of the <see cref="XmpProfile"/> class from an XML document.
32+ /// The document is serialized as UTF-8 without BOM .
3333 /// </summary>
34- /// <param name="other ">The other XMP profile, from which the clone should be made from .</param>
35- private XmpProfile ( XmpProfile other )
34+ /// <param name="document ">The XMP XML document .</param>
35+ public XmpProfile ( XDocument document )
3636 {
37- Guard . NotNull ( other , nameof ( other ) ) ;
38-
39- this . Data = other . Data ;
37+ Guard . NotNull ( document , nameof ( document ) ) ;
38+ this . Data = SerializeDocument ( document ) ;
4039 }
4140
4241 /// <summary>
@@ -45,30 +44,28 @@ private XmpProfile(XmpProfile other)
4544 internal byte [ ] ? Data { get ; private set ; }
4645
4746 /// <summary>
48- /// Gets the raw XML document containing the XMP profile .
47+ /// Convert the content of this <see cref="XmpProfile"/> into an <see cref="XDocument"/> .
4948 /// </summary>
5049 /// <returns>The <see cref="XDocument"/></returns>
51- public XDocument ? GetDocument ( )
50+ public XDocument ? ToXDocument ( )
5251 {
53- byte [ ] ? byteArray = this . Data ;
54- if ( byteArray is null )
52+ byte [ ] ? data = this . Data ;
53+ if ( data is null || data . Length == 0 )
5554 {
5655 return null ;
5756 }
5857
59- // Strip leading whitespace, as the XmlReader doesn't like them.
60- int count = byteArray . Length ;
61- for ( int i = count - 1 ; i > 0 ; i -- )
58+ using MemoryStream stream = new ( data , writable : false ) ;
59+
60+ XmlReaderSettings settings = new ( )
6261 {
63- if ( byteArray [ i ] is 0 or 0x0f )
64- {
65- count -- ;
66- }
67- }
62+ DtdProcessing = DtdProcessing . Ignore ,
63+ XmlResolver = null ,
64+ CloseInput = false
65+ } ;
6866
69- using MemoryStream stream = new ( byteArray , 0 , count ) ;
70- using StreamReader reader = new ( stream , Encoding . UTF8 ) ;
71- return XDocument . Load ( reader ) ;
67+ using XmlReader reader = XmlReader . Create ( stream , settings ) ;
68+ return XDocument . Load ( reader , LoadOptions . PreserveWhitespace ) ;
7269 }
7370
7471 /// <summary>
@@ -84,5 +81,76 @@ public byte[] ToByteArray()
8481 }
8582
8683 /// <inheritdoc/>
87- public XmpProfile DeepClone ( ) => new ( this ) ;
84+ public XmpProfile DeepClone ( )
85+ {
86+ Guard . NotNull ( this . Data ) ;
87+
88+ byte [ ] clone = new byte [ this . Data . Length ] ;
89+ this . Data . AsSpan ( ) . CopyTo ( clone ) ;
90+ return new XmpProfile ( clone ) ;
91+ }
92+
93+ private static byte [ ] SerializeDocument ( XDocument document )
94+ {
95+ using MemoryStream ms = new ( ) ;
96+
97+ XmlWriterSettings writerSettings = new ( )
98+ {
99+ Encoding = new UTF8Encoding ( encoderShouldEmitUTF8Identifier : false ) , // no BOM
100+ OmitXmlDeclaration = true , // generally safer for XMP consumers
101+ Indent = false ,
102+ NewLineHandling = NewLineHandling . None
103+ } ;
104+
105+ using ( XmlWriter xw = XmlWriter . Create ( ms , writerSettings ) )
106+ {
107+ document . Save ( xw ) ;
108+ }
109+
110+ return ms . ToArray ( ) ;
111+ }
112+
113+ private static byte [ ] ? NormalizeDataIfNeeded ( byte [ ] ? data )
114+ {
115+ if ( data is null || data . Length == 0 )
116+ {
117+ return data ;
118+ }
119+
120+ // Allocation-free fast path for the normal case.
121+ bool hasBom = data . Length >= 3 && data [ 0 ] == 0xEF && data [ 1 ] == 0xBB && data [ 2 ] == 0xBF ;
122+ bool hasTrailingPad = data [ ^ 1 ] is 0 or 0x0F ;
123+
124+ if ( ! hasBom && ! hasTrailingPad )
125+ {
126+ return data ;
127+ }
128+
129+ int start = hasBom ? 3 : 0 ;
130+ int end = data . Length ;
131+
132+ if ( hasTrailingPad )
133+ {
134+ while ( end > start )
135+ {
136+ byte b = data [ end - 1 ] ;
137+ if ( b is not 0 and not 0x0F )
138+ {
139+ break ;
140+ }
141+
142+ end -- ;
143+ }
144+ }
145+
146+ int length = end - start ;
147+ if ( length <= 0 )
148+ {
149+ return [ ] ;
150+ }
151+
152+ byte [ ] normalized = new byte [ length ] ;
153+ Buffer . BlockCopy ( data , start , normalized , 0 , length ) ;
154+ return normalized ;
155+ }
88156}
0 commit comments