22// The .NET Foundation licenses this file to you under the MIT license.
33
44using System ;
5+ #if NET
6+ using System . Buffers ;
7+ using System . Buffers . Text ;
8+ #endif
59using System . Diagnostics ;
610using System . Diagnostics . CodeAnalysis ;
11+ #if ! NET
12+ using System . Runtime . InteropServices ;
13+ #endif
714using System . Text . Json . Serialization ;
815using Microsoft . Shared . Diagnostics ;
916
1017#pragma warning disable S3996 // URI properties should not be strings
1118#pragma warning disable CA1054 // URI-like parameters should not be strings
1219#pragma warning disable CA1056 // URI-like properties should not be strings
20+ #pragma warning disable CA1307 // Specify StringComparison for clarity
1321
1422namespace Microsoft . Extensions . AI ;
1523
@@ -70,39 +78,35 @@ public DataContent(Uri uri, string? mediaType = null)
7078 [ JsonConstructor ]
7179 public DataContent ( [ StringSyntax ( StringSyntaxAttribute . Uri ) ] string uri , string ? mediaType = null )
7280 {
81+ // Store and validate the data URI.
7382 _uri = Throw . IfNullOrWhitespace ( uri ) ;
74-
7583 if ( ! uri . StartsWith ( DataUriParser . Scheme , StringComparison . OrdinalIgnoreCase ) )
7684 {
7785 Throw . ArgumentException ( nameof ( uri ) , "The provided URI is not a data URI." ) ;
7886 }
7987
88+ // Parse the data URI to extract the data and media type.
8089 _dataUri = DataUriParser . Parse ( uri . AsMemory ( ) ) ;
8190
91+ // Validate and store the media type.
92+ mediaType ??= _dataUri . MediaType ;
8293 if ( mediaType is null )
8394 {
84- mediaType = _dataUri . MediaType ;
85- if ( mediaType is null )
86- {
87- Throw . ArgumentNullException ( nameof ( mediaType ) , $ "{ nameof ( uri ) } did not contain a media type, and { nameof ( mediaType ) } was not provided.") ;
88- }
89- }
90- else
91- {
92- if ( mediaType != _dataUri . MediaType )
93- {
94- // If the data URI contains a media type that's different from a non-null media type
95- // explicitly provided, prefer the one explicitly provided as an override.
96-
97- // Extract the bytes from the data URI and null out the uri.
98- // Then we'll lazily recreate it later if needed based on the updated media type.
99- _data = _dataUri . ToByteArray ( ) ;
100- _dataUri = null ;
101- _uri = null ;
102- }
95+ Throw . ArgumentNullException ( nameof ( mediaType ) , $ "{ nameof ( uri ) } did not contain a media type, and { nameof ( mediaType ) } was not provided.") ;
10396 }
10497
10598 MediaType = DataUriParser . ThrowIfInvalidMediaType ( mediaType ) ;
99+
100+ if ( ! _dataUri . IsBase64 || mediaType != _dataUri . MediaType )
101+ {
102+ // In rare cases, the data URI may contain non-base64 data, in which case we
103+ // want to normalize it to base64. The supplied media type may also be different
104+ // from the one in the data URI. In either case, we extract the bytes from the data URI
105+ // and then throw away the uri; we'll recreate it lazily in the canonical form.
106+ _data = _dataUri . ToByteArray ( ) ;
107+ _dataUri = null ;
108+ _uri = null ;
109+ }
106110 }
107111
108112 /// <summary>
@@ -134,9 +138,8 @@ public DataContent(ReadOnlyMemory<byte> data, string mediaType)
134138
135139 /// <summary>Gets the data URI for this <see cref="DataContent"/>.</summary>
136140 /// <remarks>
137- /// The returned URI is always a valid URI string, even if the instance was constructed from a <see cref="ReadOnlyMemory{Byte}"/>
138- /// or from a <see cref="System.Uri"/>. In the case of a <see cref="ReadOnlyMemory{T}"/>, this property returns a data URI containing
139- /// that data.
141+ /// The returned URI is always a valid data URI string, even if the instance was constructed from a <see cref="ReadOnlyMemory{Byte}"/>
142+ /// or from a <see cref="System.Uri"/>.
140143 /// </remarks>
141144 [ StringSyntax ( StringSyntaxAttribute . Uri ) ]
142145 public string Uri
@@ -145,27 +148,26 @@ public string Uri
145148 {
146149 if ( _uri is null )
147150 {
148- if ( _dataUri is null )
149- {
150- Debug . Assert ( _data is not null , "Expected _data to be initialized." ) ;
151- _uri = string . Concat ( "data:" , MediaType , ";base64," , Convert . ToBase64String ( _data . GetValueOrDefault ( )
152- #if NET
153- . Span ) ) ;
154- #else
155- . Span . ToArray ( ) ) ) ;
156- #endif
157- }
158- else
159- {
160- _uri = _dataUri . IsBase64 ?
151+ Debug . Assert ( _data is not null , "Expected _data to be initialized." ) ;
152+ ReadOnlyMemory < byte > data = _data . GetValueOrDefault ( ) ;
153+
161154#if NET
162- $ "data:{ MediaType } ;base64,{ _dataUri . Data . Span } " :
163- $ "data:{ MediaType } ;,{ _dataUri . Data . Span } ";
155+ char [ ] array = ArrayPool < char > . Shared . Rent (
156+ "data:" . Length + MediaType . Length + ";base64," . Length + Base64 . GetMaxEncodedToUtf8Length ( data . Length ) ) ;
157+
158+ bool wrote = array . AsSpan ( ) . TryWrite ( $ "data:{ MediaType } ;base64,", out int prefixLength ) ;
159+ wrote |= Convert . TryToBase64Chars ( data . Span , array . AsSpan ( prefixLength ) , out int dataLength ) ;
160+ Debug . Assert ( wrote , "Expected to successfully write the data URI." ) ;
161+ _uri = array . AsSpan ( 0 , prefixLength + dataLength ) . ToString ( ) ;
162+
163+ ArrayPool < char > . Shared . Return ( array ) ;
164164#else
165- $"data:{MediaType};base64,{_dataUri.Data}" :
166- $"data:{MediaType};,{_dataUri.Data}" ;
165+ string base64 = MemoryMarshal . TryGetArray ( data , out ArraySegment < byte > segment ) ?
166+ Convert . ToBase64String ( segment . Array ! , segment . Offset , segment . Count ) :
167+ Convert . ToBase64String ( data . ToArray ( ) ) ;
168+
169+ _uri = $ "data:{ MediaType } ;base64,{ base64 } ";
167170#endif
168- }
169171 }
170172
171173 return _uri ;
@@ -205,6 +207,20 @@ public ReadOnlyMemory<byte> Data
205207 }
206208 }
207209
210+ /// <summary>Gets the data represented by this instance as a Base64 character sequence.</summary>
211+ /// <returns>The base64 representation of the data.</returns>
212+ [ JsonIgnore ]
213+ public ReadOnlyMemory < char > Base64Data
214+ {
215+ get
216+ {
217+ string uri = Uri ;
218+ int pos = uri . IndexOf ( ',' ) ;
219+ Debug . Assert ( pos >= 0 , "Expected comma to be present in the URI." ) ;
220+ return uri . AsMemory ( pos + 1 ) ;
221+ }
222+ }
223+
208224 /// <summary>Gets a string representing this instance to display in the debugger.</summary>
209225 [ DebuggerBrowsable ( DebuggerBrowsableState . Never ) ]
210226 private string DebuggerDisplay
0 commit comments