Skip to content

Commit a48fd9d

Browse files
committed
Adds support for data image uri with encoded svg
This supports SVG as base64 encoded data blob or inline tags. This is an alternative to #550 which did not use the new image API at all.
1 parent f3da867 commit a48fd9d

File tree

3 files changed

+85
-30
lines changed

3 files changed

+85
-30
lines changed

example/lib/main.dart

Lines changed: 6 additions & 6 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(
@@ -162,8 +163,7 @@ class _MyHomePageState extends State<MyHomePage> {
162163
),
163164
// On relative paths starting with /wiki, prefix with a base url
164165
(attr, _) => attr["src"] != null && attr["src"].startsWith("/wiki"):
165-
networkImageRender(
166-
mapUrl: (url) => "https://upload.wikimedia.org" + url),
166+
networkImageRender(mapUrl: (url) => "https://upload.wikimedia.org" + url),
167167
// Custom placeholder image for broken links
168168
networkSourceMatcher(): networkImageRender(altWidget: (_) => FlutterLogo()),
169169
},

lib/image_render.dart

Lines changed: 40 additions & 22 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"],
@@ -33,8 +38,8 @@ ImageSourceMatcher networkSourceMatcher({
3338
}
3439
};
3540

36-
ImageSourceMatcher assetUriMatcher() => (attributes, element) =>
37-
_src(attributes) != null && _src(attributes).startsWith("asset:");
41+
ImageSourceMatcher assetUriMatcher() =>
42+
(attributes, element) => _src(attributes) != null && _src(attributes).startsWith("asset:");
3843

3944
typedef ImageRender = Widget Function(
4045
RenderContext context,
@@ -43,8 +48,7 @@ typedef ImageRender = Widget Function(
4348
);
4449

4550
ImageRender base64ImageRender() => (context, attributes, element) {
46-
final decodedImage =
47-
base64.decode(_src(attributes).split("base64,")[1].trim());
51+
final decodedImage = base64.decode(_src(attributes).split("base64,")[1].trim());
4852
precacheImage(
4953
MemoryImage(decodedImage),
5054
context.buildContext,
@@ -56,8 +60,7 @@ ImageRender base64ImageRender() => (context, attributes, element) {
5660
decodedImage,
5761
frameBuilder: (ctx, child, frame, _) {
5862
if (frame == null) {
59-
return Text(_alt(attributes) ?? "",
60-
style: context.style.generateTextStyle());
63+
return Text(_alt(attributes) ?? "", style: context.style.generateTextStyle());
6164
}
6265
return child;
6366
},
@@ -79,8 +82,7 @@ ImageRender assetImageRender({
7982
height: height ?? _height(attributes),
8083
frameBuilder: (ctx, child, frame, _) {
8184
if (frame == null) {
82-
return Text(_alt(attributes) ?? "",
83-
style: context.style.generateTextStyle());
85+
return Text(_alt(attributes) ?? "", style: context.style.generateTextStyle());
8486
}
8587
return child;
8688
},
@@ -109,8 +111,7 @@ ImageRender networkImageRender({
109111
},
110112
);
111113
Completer<Size> completer = Completer();
112-
Image image =
113-
Image.network(src, frameBuilder: (ctx, child, frame, _) {
114+
Image image = Image.network(src, frameBuilder: (ctx, child, frame, _) {
114115
if (frame == null) {
115116
if (!completer.isCompleted) {
116117
completer.completeError("error");
@@ -124,8 +125,7 @@ ImageRender networkImageRender({
124125
image.image.resolve(ImageConfiguration()).addListener(
125126
ImageStreamListener((ImageInfo image, bool synchronousCall) {
126127
var myImage = image.image;
127-
Size size =
128-
Size(myImage.width.toDouble(), myImage.height.toDouble());
128+
Size size = Size(myImage.width.toDouble(), myImage.height.toDouble());
129129
if (!completer.isCompleted) {
130130
completer.complete(size);
131131
}
@@ -147,28 +147,46 @@ ImageRender networkImageRender({
147147
frameBuilder: (ctx, child, frame, _) {
148148
if (frame == null) {
149149
return altWidget?.call(_alt(attributes)) ??
150-
Text(_alt(attributes) ?? "",
151-
style: context.style.generateTextStyle());
150+
Text(_alt(attributes) ?? "", style: context.style.generateTextStyle());
152151
}
153152
return child;
154153
},
155154
);
156155
} else if (snapshot.hasError) {
157-
return altWidget?.call(_alt(attributes)) ?? Text(_alt(attributes) ?? "",
158-
style: context.style.generateTextStyle());
156+
return altWidget?.call(_alt(attributes)) ??
157+
Text(_alt(attributes) ?? "", style: context.style.generateTextStyle());
159158
} else {
160159
return loadingWidget?.call() ?? const CircularProgressIndicator();
161160
}
162161
},
163162
);
164163
};
165164

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

170187
final Map<ImageSourceMatcher, ImageRender> defaultImageRenders = {
171-
base64DataUriMatcher(): base64ImageRender(),
188+
dataUriMatcher(mime: 'image/svg+xml', encoding: null): svgDataImageRender(),
189+
dataUriMatcher(): base64ImageRender(),
172190
assetUriMatcher(): assetImageRender(),
173191
networkSourceMatcher(extension: "svg"): svgNetworkImageRender(),
174192
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)