Skip to content

Commit 0bd3132

Browse files
committed
Extend custom image API with svg support
Also swallow missing sources silently and add several examples
1 parent 1f40f96 commit 0bd3132

File tree

7 files changed

+145
-71
lines changed

7 files changed

+145
-71
lines changed

example/assets/html5.png

40.6 KB
Loading

example/assets/mac.svg

Lines changed: 3 additions & 0 deletions
Loading

example/ios/Podfile.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,4 @@ SPEC CHECKSUMS:
3131

3232
PODFILE CHECKSUM: 8e679eca47255a8ca8067c4c67aab20e64cb974d
3333

34-
COCOAPODS: 1.10.0
34+
COCOAPODS: 1.10.1

example/lib/main.dart

Lines changed: 26 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import 'package:flutter/material.dart';
22
import 'package:flutter_html/flutter_html.dart';
3-
import 'package:flutter_html/html_parser.dart';
43
import 'package:flutter_html/image_render.dart';
5-
import 'package:flutter_html/style.dart';
64

75
void main() => runApp(new MyApp());
86

@@ -119,11 +117,31 @@ const htmlData = """
119117
Linking to <a href='https://github.com'>websites</a> has never been easier.
120118
</p>
121119
<h3>Image support:</h3>
122-
<p>
123-
<img alt='Flutter' src='https://flutter.dev/assets/flutter-lockup-1caf6476beed76adec3c477586da54de6b552b2f42108ec5bc68dc63bae2df75.png' />
124-
<img alt='Google' src='https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_92x30dp.png' />
125-
<a href='https://google.com'><img alt='Google' src='https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_92x30dp.png' /></a>
126-
</p>
120+
<h3>Network png</h3>
121+
<img alt='Google' src='https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_92x30dp.png' />
122+
<h3>Network svg</h3>
123+
<img src='https://dev.w3.org/SVG/tools/svgweb/samples/svg-files/android.svg' />
124+
<h3>Local asset png</h3>
125+
<img src='asset:assets/html5.png' width='100' />
126+
<h3>Local asset svg</h3>
127+
<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==' />
130+
<h3>Custom image render (flutter.dev)</h3>
131+
<img src='https://flutter.dev/images/flutter-mono-81x100.png' />
132+
<h3>No image source</h3>
133+
<img alt='No source' />
134+
<img alt='Empty source' src='' />
135+
<h3>Broken network image</h3>
136+
<img alt='Broken image' src='https://www.notgoogle.com/images/branding/googlelogo/2x/googlelogo_color_92x30dp.png' />
137+
<h3>Used inside a table</h3>
138+
<table>
139+
<tr>
140+
<td><img alt='Google' src='https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_92x30dp.png' /></td>
141+
<td><img alt='Google' src='https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_92x30dp.png' /></td>
142+
<td><img alt='Google' src='https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_92x30dp.png' /></td>
143+
</tr>
144+
</table>
127145
<h3>Video support:</h3>
128146
<video controls>
129147
<source src="https://www.w3schools.com/html/mov_bbb.mp4" />
@@ -148,50 +166,12 @@ class _MyHomePageState extends State<MyHomePage> {
148166
child: Html(
149167
data: htmlData,
150168
//Optional parameters:
151-
style: {
152-
"html": Style(
153-
backgroundColor: Colors.black12,
154-
// color: Colors.white,
155-
),
156-
// "h1": Style(
157-
// textAlign: TextAlign.center,
158-
// ),
159-
"table": Style(
160-
backgroundColor: Color.fromARGB(0x50, 0xee, 0xee, 0xee),
161-
),
162-
"tr": Style(
163-
border: Border(bottom: BorderSide(color: Colors.grey)),
164-
),
165-
"th": Style(
166-
padding: EdgeInsets.all(6),
167-
backgroundColor: Colors.grey,
168-
),
169-
"td": Style(
170-
padding: EdgeInsets.all(6),
171-
alignment: Alignment.topLeft,
172-
),
173-
"var": Style(fontFamily: 'serif'),
174-
},
175-
customRender: {
176-
"bird": (RenderContext context, Widget child, attributes, _) {
177-
return TextSpan(text: "🐦");
178-
},
179-
"flutter": (RenderContext context, Widget child, attributes, _) {
180-
return FlutterLogo(
181-
style: (attributes['horizontal'] != null)
182-
? FlutterLogoStyle.horizontal
183-
: FlutterLogoStyle.markOnly,
184-
textColor: context.style.color,
185-
size: context.style.fontSize.size * 5,
186-
);
187-
},
188-
},
189169
customImageRenders: {
190170
networkSourceMatcher(domains: ["flutter.dev"]):
191171
(context, attributes, element) {
192172
return FlutterLogo(size: 36);
193173
},
194-
networkSourceMatcher(): networkImageRender(
174+
networkSourceMatcher(domains: ["mydomain.com"]): networkImageRender(
195175
headers: {"Custom-Header": "some-value"},
196176
altWidget: (alt) => Text(alt),
197177
),

example/pubspec.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,7 @@ dev_dependencies:
1919
flutter:
2020

2121
uses-material-design: true
22+
23+
assets:
24+
- assets/html5.png
25+
- assets/mac.svg

lib/image_render.dart

Lines changed: 110 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'dart:async';
12
import 'dart:convert';
23

34
import 'package:flutter/material.dart';
@@ -11,21 +12,30 @@ typedef ImageSourceMatcher = bool Function(
1112
);
1213

1314
ImageSourceMatcher base64UriMatcher() => (attributes, element) =>
14-
attributes["src"].startsWith("data:image") &&
15-
attributes["src"].contains("base64,");
15+
_src(attributes) != null &&
16+
_src(attributes).startsWith("data:image") &&
17+
_src(attributes).contains("base64,");
1618

1719
ImageSourceMatcher networkSourceMatcher({
1820
List<String> schemas: const ["https", "http"],
1921
List<String> domains,
2022
String extension,
2123
}) =>
2224
(attributes, element) {
23-
final src = Uri.parse(attributes["src"]);
24-
return schemas.contains(src.scheme) &&
25-
(domains == null || domains.contains(src.host)) &&
26-
(extension == null || src.path.endsWith(".$extension"));
25+
if (_src(attributes) == null) return false;
26+
try {
27+
final src = Uri.parse(_src(attributes));
28+
return schemas.contains(src.scheme) &&
29+
(domains == null || domains.contains(src.host)) &&
30+
(extension == null || src.path.endsWith(".$extension"));
31+
} catch (e) {
32+
return false;
33+
}
2734
};
2835

36+
ImageSourceMatcher assetUriMatcher() => (attributes, element) =>
37+
_src(attributes) != null && _src(attributes).startsWith("asset:");
38+
2939
typedef ImageRender = Widget Function(
3040
RenderContext context,
3141
Map<String, String> attributes,
@@ -34,7 +44,7 @@ typedef ImageRender = Widget Function(
3444

3545
ImageRender base64ImageRender() => (context, attributes, element) {
3646
final decodedImage =
37-
base64.decode(attributes["src"].split("base64,")[1].trim());
47+
base64.decode(_src(attributes).split("base64,")[1].trim());
3848
precacheImage(
3949
MemoryImage(decodedImage),
4050
context.buildContext,
@@ -46,14 +56,38 @@ ImageRender base64ImageRender() => (context, attributes, element) {
4656
decodedImage,
4757
frameBuilder: (ctx, child, frame, _) {
4858
if (frame == null) {
49-
return Text(attributes["alt"] ?? "",
59+
return Text(_alt(attributes) ?? "",
5060
style: context.style.generateTextStyle());
5161
}
5262
return child;
5363
},
5464
);
5565
};
5666

67+
ImageRender assetImageRender({
68+
double width,
69+
double height,
70+
}) =>
71+
(context, attributes, element) {
72+
final assetPath = _src(attributes).replaceFirst('asset:', '');
73+
if (_src(attributes).endsWith(".svg")) {
74+
return SvgPicture.asset(assetPath);
75+
} else {
76+
return Image.asset(
77+
assetPath,
78+
width: width ?? _width(attributes),
79+
height: height ?? _height(attributes),
80+
frameBuilder: (ctx, child, frame, _) {
81+
if (frame == null) {
82+
return Text(_alt(attributes) ?? "",
83+
style: context.style.generateTextStyle());
84+
}
85+
return child;
86+
},
87+
);
88+
}
89+
};
90+
5791
ImageRender networkImageRender({
5892
Map<String, String> headers,
5993
double width,
@@ -62,36 +96,89 @@ ImageRender networkImageRender({
6296
}) =>
6397
(context, attributes, element) {
6498
precacheImage(
65-
NetworkImage(attributes["src"]),
99+
NetworkImage(
100+
_src(attributes),
101+
headers: headers,
102+
),
66103
context.buildContext,
67104
onError: (exception, StackTrace stackTrace) {
68105
context.parser.onImageError?.call(exception, stackTrace);
69106
},
70107
);
71-
return Image.network(
72-
attributes["src"],
73-
headers: headers,
74-
width: width,
75-
height: height,
76-
frameBuilder: (ctx, child, frame, _) {
77-
if (frame == null) {
78-
return altWidget.call(attributes["alt"]) ??
79-
Text(attributes["alt"] ?? "",
80-
style: context.style.generateTextStyle());
81-
}
108+
Completer<Size> completer = Completer();
109+
Image image =
110+
Image.network(_src(attributes), frameBuilder: (ctx, child, frame, _) {
111+
if (frame == null) {
112+
completer.completeError("error");
82113
return child;
114+
} else {
115+
return child;
116+
}
117+
});
118+
119+
image.image.resolve(ImageConfiguration()).addListener(
120+
ImageStreamListener((ImageInfo image, bool synchronousCall) {
121+
var myImage = image.image;
122+
Size size =
123+
Size(myImage.width.toDouble(), myImage.height.toDouble());
124+
completer.complete(size);
125+
}, onError: (object, stacktrace) {
126+
completer.completeError(object);
127+
}),
128+
);
129+
return FutureBuilder<Size>(
130+
future: completer.future,
131+
builder: (BuildContext buildContext, AsyncSnapshot<Size> snapshot) {
132+
if (snapshot.hasData) {
133+
return Image.network(
134+
_src(attributes),
135+
headers: headers,
136+
width: width ?? _width(attributes) ?? snapshot.data.width,
137+
height: height ?? _height(attributes),
138+
frameBuilder: (ctx, child, frame, _) {
139+
if (frame == null) {
140+
return altWidget.call(_alt(attributes)) ??
141+
Text(_alt(attributes) ?? "",
142+
style: context.style.generateTextStyle());
143+
}
144+
return child;
145+
},
146+
);
147+
} else if (snapshot.hasError) {
148+
return Text(_alt(attributes) ?? "",
149+
style: context.style.generateTextStyle());
150+
} else {
151+
return new CircularProgressIndicator();
152+
}
83153
},
84154
);
85155
};
86156

87157
ImageRender svgNetworkImageRender() => (context, attributes, element) {
88-
return SvgPicture.network(
89-
attributes["src"],
90-
);
158+
return SvgPicture.network(attributes["src"]);
91159
};
92160

93161
final Map<ImageSourceMatcher, ImageRender> defaultImageRenders = {
94162
base64UriMatcher(): base64ImageRender(),
163+
assetUriMatcher(): assetImageRender(),
95164
networkSourceMatcher(extension: "svg"): svgNetworkImageRender(),
96165
networkSourceMatcher(): networkImageRender(),
97166
};
167+
168+
String _src(Map<String, String> attributes) {
169+
return attributes["src"];
170+
}
171+
172+
String _alt(Map<String, String> attributes) {
173+
return attributes["alt"];
174+
}
175+
176+
double _height(Map<String, String> attributes) {
177+
final heightString = attributes["height"];
178+
return heightString == null ? heightString : double.tryParse(heightString);
179+
}
180+
181+
double _width(Map<String, String> attributes) {
182+
final widthString = attributes["width"];
183+
return widthString == null ? widthString : double.tryParse(widthString);
184+
}

lib/src/replaced_element.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ class ImageContentElement extends ReplacedElement {
8686
}
8787
}
8888
}
89-
return Container();
89+
return SizedBox(width: 0, height: 0);
9090
}
9191
}
9292

0 commit comments

Comments
 (0)