Skip to content

Commit 15c5cb5

Browse files
authored
Merge pull request Sub6Resources#551 from vrtdev/feature/data-uri-svg
Adds support for data image uri with encoded svg
2 parents 8200613 + 85decd4 commit 15c5cb5

File tree

3 files changed

+82
-24
lines changed

3 files changed

+82
-24
lines changed

example/lib/main.dart

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,10 @@ const htmlData = """
125125
<img src='asset:assets/html5.png' width='100' />
126126
<h3>Local asset svg</h3>
127127
<img src='asset:assets/mac.svg' width='100' />
128-
<h3>Base64</h3>
129-
<img alt='Red dot' src='data:image/png;base64, iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==' />
128+
<h3>Data uri (with base64 support)</h3>
129+
<img alt='Red dot (png)' src='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==' />
130+
<img alt='Green dot (base64 svg)' src='data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB2aWV3Qm94PSIwIDAgMzAgMjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxjaXJjbGUgY3g9IjE1IiBjeT0iMTAiIHI9IjEwIiBmaWxsPSJncmVlbiIvPgo8L3N2Zz4=' />
131+
<img alt='Green dot (plain svg)' src='data:image/svg+xml,%3C?xml version="1.0" encoding="UTF-8"?%3E%3Csvg viewBox="0 0 30 20" xmlns="http://www.w3.org/2000/svg"%3E%3Ccircle cx="15" cy="10" r="10" fill="yellow"/%3E%3C/svg%3E' />
130132
<h3>Custom source matcher (relative paths)</h3>
131133
<img src='/wikipedia/commons/thumb/e/ef/Octicons-logo-github.svg/200px-Octicons-logo-github.svg.png' />
132134
<h3>Custom image render (flutter.dev)</h3>
@@ -151,8 +153,7 @@ class _MyHomePageState extends State<MyHomePage> {
151153
data: htmlData,
152154
//Optional parameters:
153155
customImageRenders: {
154-
networkSourceMatcher(domains: ["flutter.dev"]):
155-
(context, attributes, element) {
156+
networkSourceMatcher(domains: ["flutter.dev"]): (context, attributes, element) {
156157
return FlutterLogo(size: 36);
157158
},
158159
networkSourceMatcher(domains: ["mydomain.com"]): networkImageRender(

lib/image_render.dart

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,15 @@ typedef ImageSourceMatcher = bool Function(
1111
dom.Element? element,
1212
);
1313

14-
ImageSourceMatcher base64DataUriMatcher() => (attributes, element) =>
15-
_src(attributes) != null &&
16-
_src(attributes)!.startsWith("data:image") &&
17-
_src(attributes)!.contains("base64,");
14+
final _dataUriFormat = RegExp("^(?<scheme>data):(?<mime>image\/[\\w\+\-\.]+)(?<encoding>;base64)?\,(?<data>.*)");
15+
16+
ImageSourceMatcher dataUriMatcher({String? encoding = 'base64', String? mime}) => (attributes, element) {
17+
if (_src(attributes) == null) return false;
18+
final dataUri = _dataUriFormat.firstMatch(_src(attributes)!);
19+
return dataUri != null &&
20+
(mime == null || dataUri.namedGroup('mime') == mime) &&
21+
(encoding == null || dataUri.namedGroup('encoding') == ';$encoding');
22+
};
1823

1924
ImageSourceMatcher networkSourceMatcher({
2025
List<String> schemas: const ["https", "http"],
@@ -56,8 +61,7 @@ ImageRender base64ImageRender() => (context, attributes, element) {
5661
decodedImage,
5762
frameBuilder: (ctx, child, frame, _) {
5863
if (frame == null) {
59-
return Text(_alt(attributes) ?? "",
60-
style: context.style.generateTextStyle());
64+
return Text(_alt(attributes) ?? "", style: context.style.generateTextStyle());
6165
}
6266
return child;
6367
},
@@ -79,8 +83,7 @@ ImageRender assetImageRender({
7983
height: height ?? _height(attributes),
8084
frameBuilder: (ctx, child, frame, _) {
8185
if (frame == null) {
82-
return Text(_alt(attributes) ?? "",
83-
style: context.style.generateTextStyle());
86+
return Text(_alt(attributes) ?? "", style: context.style.generateTextStyle());
8487
}
8588
return child;
8689
},
@@ -109,8 +112,7 @@ ImageRender networkImageRender({
109112
},
110113
);
111114
Completer<Size> completer = Completer();
112-
Image image =
113-
Image.network(src, frameBuilder: (ctx, child, frame, _) {
115+
Image image = Image.network(src, frameBuilder: (ctx, child, frame, _) {
114116
if (frame == null) {
115117
if (!completer.isCompleted) {
116118
completer.completeError("error");
@@ -124,8 +126,7 @@ ImageRender networkImageRender({
124126
image.image.resolve(ImageConfiguration()).addListener(
125127
ImageStreamListener((ImageInfo image, bool synchronousCall) {
126128
var myImage = image.image;
127-
Size size =
128-
Size(myImage.width.toDouble(), myImage.height.toDouble());
129+
Size size = Size(myImage.width.toDouble(), myImage.height.toDouble());
129130
if (!completer.isCompleted) {
130131
completer.complete(size);
131132
}
@@ -147,28 +148,47 @@ ImageRender networkImageRender({
147148
frameBuilder: (ctx, child, frame, _) {
148149
if (frame == null) {
149150
return altWidget?.call(_alt(attributes)) ??
150-
Text(_alt(attributes) ?? "",
151-
style: context.style.generateTextStyle());
151+
Text(_alt(attributes) ?? "", style: context.style.generateTextStyle());
152152
}
153153
return child;
154154
},
155155
);
156156
} else if (snapshot.hasError) {
157-
return altWidget?.call(_alt(attributes)) ?? Text(_alt(attributes) ?? "",
158-
style: context.style.generateTextStyle());
157+
return altWidget?.call(_alt(attributes)) ??
158+
Text(_alt(attributes) ?? "", style: context.style.generateTextStyle());
159159
} else {
160160
return loadingWidget?.call() ?? const CircularProgressIndicator();
161161
}
162162
},
163163
);
164164
};
165165

166+
ImageRender svgDataImageRender() => (context, attributes, element) {
167+
final dataUri = _dataUriFormat.firstMatch(_src(attributes)!);
168+
final data = dataUri?.namedGroup('data');
169+
if (data == null) return null;
170+
if (dataUri?.namedGroup('encoding') == ';base64') {
171+
final decodedImage = base64.decode(data.trim());
172+
return SvgPicture.memory(
173+
decodedImage,
174+
width: _width(attributes),
175+
height: _height(attributes),
176+
);
177+
}
178+
return SvgPicture.string(Uri.decodeFull(data));
179+
};
180+
166181
ImageRender svgNetworkImageRender() => (context, attributes, element) {
167-
return SvgPicture.network(_src(attributes)!);
182+
return SvgPicture.network(
183+
attributes["src"]!,
184+
width: _width(attributes),
185+
height: _height(attributes),
186+
);
168187
};
169188

170189
final Map<ImageSourceMatcher, ImageRender> defaultImageRenders = {
171-
base64DataUriMatcher(): base64ImageRender(),
190+
dataUriMatcher(mime: 'image/svg+xml', encoding: null): svgDataImageRender(),
191+
dataUriMatcher(): base64ImageRender(),
172192
assetUriMatcher(): assetImageRender(),
173193
networkSourceMatcher(extension: "svg"): svgNetworkImageRender(),
174194
networkSourceMatcher(): networkImageRender(),

test/image_render_source_matcher_test.dart

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,8 @@ void main() {
7979
expect(_match(matcher, ''), isFalse);
8080
});
8181
});
82-
group("base64 image data uri matcher", () {
83-
ImageSourceMatcher matcher = base64DataUriMatcher();
82+
group("default (base64) image data uri matcher", () {
83+
ImageSourceMatcher matcher = dataUriMatcher();
8484
test("matches a full png base64 data uri", () {
8585
expect(
8686
_match(matcher,
@@ -115,6 +115,43 @@ void main() {
115115
expect(_match(matcher, ''), isFalse);
116116
});
117117
});
118+
group("custom image data uri matcher", () {
119+
ImageSourceMatcher matcher =
120+
dataUriMatcher(encoding: null, mime: 'image/svg+xml');
121+
test("matches an svg data uri with base64 encoding", () {
122+
expect(
123+
_match(matcher,
124+
'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB2aWV3Qm94PSIwIDAgMzAgMjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxjaXJjbGUgY3g9IjE1IiBjeT0iMTAiIHI9IjEwIiBmaWxsPSJncmVlbiIvPgo8L3N2Zz4='),
125+
isTrue);
126+
});
127+
test("matches an svg data uri without specified encoding", () {
128+
expect(
129+
_match(matcher,
130+
'data:image/svg+xml,%3C?xml version="1.0" encoding="UTF-8"?%3E%3Csvg viewBox="0 0 30 20" xmlns="http://www.w3.org/2000/svg"%3E%3Ccircle cx="15" cy="10" r="10" fill="green"/%3E%3C/svg%3E'),
131+
isTrue);
132+
});
133+
test("matches base64 data uri without data", () {
134+
expect(_match(matcher, 'data:image/svg+xml;base64,'), isTrue);
135+
});
136+
test("doesn't match non-base64 image data uri", () {
137+
expect(
138+
_match(matcher,
139+
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=='),
140+
isFalse);
141+
});
142+
test("doesn't match different mime data uri", () {
143+
expect(_match(matcher, 'data:text/plain;base64,'), isFalse);
144+
});
145+
test("doesn't non-data schema", () {
146+
expect(_match(matcher, 'http:'), isFalse);
147+
});
148+
test("doesn't match null", () {
149+
expect(_match(matcher, null), isFalse);
150+
});
151+
test("doesn't match empty", () {
152+
expect(_match(matcher, ''), isFalse);
153+
});
154+
});
118155
}
119156

120157
dom.Element _fakeElement(String? src) {

0 commit comments

Comments
 (0)