Skip to content

Commit e8f50ed

Browse files
authored
Merge pull request Sub6Resources#505 from vrtdev/feature/498-image-render-api
Image render API
2 parents e790aaa + 035c0a2 commit e8f50ed

File tree

10 files changed

+379
-159
lines changed

10 files changed

+379
-159
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: 34 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import 'package:flutter/material.dart';
22
import 'package:flutter_html/flutter_html.dart';
3-
import 'package:flutter_html/html_parser.dart';
4-
import 'package:flutter_html/style.dart';
3+
import 'package:flutter_html/image_render.dart';
54

65
void main() => runApp(new MyApp());
76

@@ -118,11 +117,31 @@ const htmlData = """
118117
Linking to <a href='https://github.com'>websites</a> has never been easier.
119118
</p>
120119
<h3>Image support:</h3>
121-
<p>
122-
<img alt='Google' src='https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_92x30dp.png' /><br />
123-
<a href='https://google.com'>A linked image: <img alt='Google' src='https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_92x30dp.png' /></a>
124-
<img alt='Alt Text of an intentionally broken image' src='https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_92x30d' />
125-
</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>
126145
<h3>Video support:</h3>
127146
<video controls>
128147
<source src="https://www.w3schools.com/html/mov_bbb.mp4" />
@@ -147,43 +166,15 @@ class _MyHomePageState extends State<MyHomePage> {
147166
child: Html(
148167
data: htmlData,
149168
//Optional parameters:
150-
style: {
151-
"html": Style(
152-
backgroundColor: Colors.black12,
153-
// color: Colors.white,
154-
),
155-
// "h1": Style(
156-
// textAlign: TextAlign.center,
157-
// ),
158-
"table": Style(
159-
backgroundColor: Color.fromARGB(0x50, 0xee, 0xee, 0xee),
160-
),
161-
"tr": Style(
162-
border: Border(bottom: BorderSide(color: Colors.grey)),
163-
),
164-
"th": Style(
165-
padding: EdgeInsets.all(6),
166-
backgroundColor: Colors.grey,
167-
),
168-
"td": Style(
169-
padding: EdgeInsets.all(6),
170-
alignment: Alignment.topLeft,
171-
),
172-
"var": Style(fontFamily: 'serif'),
173-
},
174-
customRender: {
175-
"bird": (RenderContext context, Widget child, attributes, _) {
176-
return TextSpan(text: "🐦");
177-
},
178-
"flutter": (RenderContext context, Widget child, attributes, _) {
179-
return FlutterLogo(
180-
style: (attributes['horizontal'] != null)
181-
? FlutterLogoStyle.horizontal
182-
: FlutterLogoStyle.markOnly,
183-
textColor: context.style.color,
184-
size: context.style.fontSize.size * 5,
185-
);
169+
customImageRenders: {
170+
networkSourceMatcher(domains: ["flutter.dev"]):
171+
(context, attributes, element) {
172+
return FlutterLogo(size: 36);
186173
},
174+
networkSourceMatcher(domains: ["mydomain.com"]): networkImageRender(
175+
headers: {"Custom-Header": "some-value"},
176+
altWidget: (alt) => Text(alt),
177+
),
187178
},
188179
onLinkTap: (url) {
189180
print("Opening $url...");

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/flutter_html.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ library flutter_html;
22

33
import 'package:flutter/material.dart';
44
import 'package:flutter_html/html_parser.dart';
5+
import 'package:flutter_html/image_render.dart';
56
import 'package:flutter_html/style.dart';
67
import 'package:webview_flutter/webview_flutter.dart';
78

@@ -36,6 +37,7 @@ class Html extends StatelessWidget {
3637
@required this.data,
3738
this.onLinkTap,
3839
this.customRender,
40+
this.customImageRenders = const {},
3941
this.onImageError,
4042
this.shrinkWrap = false,
4143
this.onImageTap,
@@ -46,6 +48,7 @@ class Html extends StatelessWidget {
4648

4749
final String data;
4850
final OnTap onLinkTap;
51+
final Map<ImageSourceMatcher, ImageRender> customImageRenders;
4952
final ImageErrorListener onImageError;
5053
final bool shrinkWrap;
5154

@@ -80,6 +83,9 @@ class Html extends StatelessWidget {
8083
shrinkWrap: shrinkWrap,
8184
style: style,
8285
customRender: customRender,
86+
imageRenders: {}
87+
..addAll(customImageRenders)
88+
..addAll(defaultImageRenders),
8389
blacklistedElements: blacklistedElements,
8490
navigationDelegateForIframe: navigationDelegateForIframe,
8591
),

lib/html_parser.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'package:csslib/visitor.dart' as css;
66
import 'package:flutter/gestures.dart';
77
import 'package:flutter/material.dart';
88
import 'package:flutter_html/flutter_html.dart';
9+
import 'package:flutter_html/image_render.dart';
910
import 'package:flutter_html/src/css_parser.dart';
1011
import 'package:flutter_html/src/html_elements.dart';
1112
import 'package:flutter_html/src/layout_element.dart';
@@ -32,6 +33,7 @@ class HtmlParser extends StatelessWidget {
3233

3334
final Map<String, Style> style;
3435
final Map<String, CustomRender> customRender;
36+
final Map<ImageSourceMatcher, ImageRender> imageRenders;
3537
final List<String> blacklistedElements;
3638
final NavigationDelegate navigationDelegateForIframe;
3739

@@ -43,6 +45,7 @@ class HtmlParser extends StatelessWidget {
4345
this.shrinkWrap,
4446
this.style,
4547
this.customRender,
48+
this.imageRenders,
4649
this.blacklistedElements,
4750
this.navigationDelegateForIframe,
4851
});

lib/image_render.dart

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
import 'dart:async';
2+
import 'dart:convert';
3+
4+
import 'package:flutter/material.dart';
5+
import 'package:flutter_html/html_parser.dart';
6+
import 'package:flutter_svg/flutter_svg.dart';
7+
import 'package:html/dom.dart' as dom;
8+
9+
typedef ImageSourceMatcher = bool Function(
10+
Map<String, String> attributes,
11+
dom.Element element,
12+
);
13+
14+
ImageSourceMatcher base64DataUriMatcher() => (attributes, element) =>
15+
_src(attributes) != null &&
16+
_src(attributes).startsWith("data:image") &&
17+
_src(attributes).contains("base64,");
18+
19+
ImageSourceMatcher networkSourceMatcher({
20+
List<String> schemas: const ["https", "http"],
21+
List<String> domains,
22+
String extension,
23+
}) =>
24+
(attributes, element) {
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+
}
34+
};
35+
36+
ImageSourceMatcher assetUriMatcher() => (attributes, element) =>
37+
_src(attributes) != null && _src(attributes).startsWith("asset:");
38+
39+
typedef ImageRender = Widget Function(
40+
RenderContext context,
41+
Map<String, String> attributes,
42+
dom.Element element,
43+
);
44+
45+
ImageRender base64ImageRender() => (context, attributes, element) {
46+
final decodedImage =
47+
base64.decode(_src(attributes).split("base64,")[1].trim());
48+
precacheImage(
49+
MemoryImage(decodedImage),
50+
context.buildContext,
51+
onError: (exception, StackTrace stackTrace) {
52+
context.parser.onImageError?.call(exception, stackTrace);
53+
},
54+
);
55+
return Image.memory(
56+
decodedImage,
57+
frameBuilder: (ctx, child, frame, _) {
58+
if (frame == null) {
59+
return Text(_alt(attributes) ?? "",
60+
style: context.style.generateTextStyle());
61+
}
62+
return child;
63+
},
64+
);
65+
};
66+
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+
91+
ImageRender networkImageRender({
92+
Map<String, String> headers,
93+
double width,
94+
double height,
95+
Widget Function(String) altWidget,
96+
}) =>
97+
(context, attributes, element) {
98+
precacheImage(
99+
NetworkImage(
100+
_src(attributes),
101+
headers: headers,
102+
),
103+
context.buildContext,
104+
onError: (exception, StackTrace stackTrace) {
105+
context.parser.onImageError?.call(exception, stackTrace);
106+
},
107+
);
108+
Completer<Size> completer = Completer();
109+
Image image =
110+
Image.network(_src(attributes), frameBuilder: (ctx, child, frame, _) {
111+
if (frame == null) {
112+
if (!completer.isCompleted) {
113+
completer.completeError("error");
114+
}
115+
return child;
116+
} else {
117+
return child;
118+
}
119+
});
120+
121+
image.image.resolve(ImageConfiguration()).addListener(
122+
ImageStreamListener((ImageInfo image, bool synchronousCall) {
123+
var myImage = image.image;
124+
Size size =
125+
Size(myImage.width.toDouble(), myImage.height.toDouble());
126+
if (!completer.isCompleted) {
127+
completer.complete(size);
128+
}
129+
}, onError: (object, stacktrace) {
130+
if (!completer.isCompleted) {
131+
completer.completeError(object);
132+
}
133+
}),
134+
);
135+
return FutureBuilder<Size>(
136+
future: completer.future,
137+
builder: (BuildContext buildContext, AsyncSnapshot<Size> snapshot) {
138+
if (snapshot.hasData) {
139+
return Image.network(
140+
_src(attributes),
141+
headers: headers,
142+
width: width ?? _width(attributes) ?? snapshot.data.width,
143+
height: height ?? _height(attributes),
144+
frameBuilder: (ctx, child, frame, _) {
145+
if (frame == null) {
146+
return altWidget.call(_alt(attributes)) ??
147+
Text(_alt(attributes) ?? "",
148+
style: context.style.generateTextStyle());
149+
}
150+
return child;
151+
},
152+
);
153+
} else if (snapshot.hasError) {
154+
return Text(_alt(attributes) ?? "",
155+
style: context.style.generateTextStyle());
156+
} else {
157+
return new CircularProgressIndicator();
158+
}
159+
},
160+
);
161+
};
162+
163+
ImageRender svgNetworkImageRender() => (context, attributes, element) {
164+
return SvgPicture.network(attributes["src"]);
165+
};
166+
167+
final Map<ImageSourceMatcher, ImageRender> defaultImageRenders = {
168+
base64DataUriMatcher(): base64ImageRender(),
169+
assetUriMatcher(): assetImageRender(),
170+
networkSourceMatcher(extension: "svg"): svgNetworkImageRender(),
171+
networkSourceMatcher(): networkImageRender(),
172+
};
173+
174+
String _src(Map<String, String> attributes) {
175+
return attributes["src"];
176+
}
177+
178+
String _alt(Map<String, String> attributes) {
179+
return attributes["alt"];
180+
}
181+
182+
double _height(Map<String, String> attributes) {
183+
final heightString = attributes["height"];
184+
return heightString == null ? heightString : double.tryParse(heightString);
185+
}
186+
187+
double _width(Map<String, String> attributes) {
188+
final widthString = attributes["width"];
189+
return widthString == null ? widthString : double.tryParse(widthString);
190+
}

0 commit comments

Comments
 (0)