Skip to content

Commit 1f40f96

Browse files
committed
Image render API proposal
1 parent 5429f07 commit 1f40f96

File tree

5 files changed

+125
-110
lines changed

5 files changed

+125
-110
lines changed

example/lib/main.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'package:flutter/material.dart';
22
import 'package:flutter_html/flutter_html.dart';
33
import 'package:flutter_html/html_parser.dart';
4+
import 'package:flutter_html/image_render.dart';
45
import 'package:flutter_html/style.dart';
56

67
void main() => runApp(new MyApp());
@@ -119,6 +120,7 @@ const htmlData = """
119120
</p>
120121
<h3>Image support:</h3>
121122
<p>
123+
<img alt='Flutter' src='https://flutter.dev/assets/flutter-lockup-1caf6476beed76adec3c477586da54de6b552b2f42108ec5bc68dc63bae2df75.png' />
122124
<img alt='Google' src='https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_92x30dp.png' />
123125
<a href='https://google.com'><img alt='Google' src='https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_92x30dp.png' /></a>
124126
</p>
@@ -184,6 +186,16 @@ class _MyHomePageState extends State<MyHomePage> {
184186
);
185187
},
186188
},
189+
customImageRenders: {
190+
networkSourceMatcher(domains: ["flutter.dev"]):
191+
(context, attributes, element) {
192+
return FlutterLogo(size: 36);
193+
},
194+
networkSourceMatcher(): networkImageRender(
195+
headers: {"Custom-Header": "some-value"},
196+
altWidget: (alt) => Text(alt),
197+
),
198+
},
187199
onLinkTap: (url) {
188200
print("Opening $url...");
189201
},

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
@@ -5,6 +5,7 @@ import 'package:csslib/parser.dart' as cssparser;
55
import 'package:csslib/visitor.dart' as css;
66
import 'package:flutter/material.dart';
77
import 'package:flutter_html/flutter_html.dart';
8+
import 'package:flutter_html/image_render.dart';
89
import 'package:flutter_html/src/css_parser.dart';
910
import 'package:flutter_html/src/html_elements.dart';
1011
import 'package:flutter_html/src/layout_element.dart';
@@ -31,6 +32,7 @@ class HtmlParser extends StatelessWidget {
3132

3233
final Map<String, Style> style;
3334
final Map<String, CustomRender> customRender;
35+
final Map<ImageSourceMatcher, ImageRender> imageRenders;
3436
final List<String> blacklistedElements;
3537
final NavigationDelegate navigationDelegateForIframe;
3638

@@ -42,6 +44,7 @@ class HtmlParser extends StatelessWidget {
4244
this.shrinkWrap,
4345
this.style,
4446
this.customRender,
47+
this.imageRenders,
4548
this.blacklistedElements,
4649
this.navigationDelegateForIframe,
4750
});

lib/image_render.dart

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import 'dart:convert';
2+
3+
import 'package:flutter/material.dart';
4+
import 'package:flutter_html/html_parser.dart';
5+
import 'package:flutter_svg/flutter_svg.dart';
6+
import 'package:html/dom.dart' as dom;
7+
8+
typedef ImageSourceMatcher = bool Function(
9+
Map<String, String> attributes,
10+
dom.Element element,
11+
);
12+
13+
ImageSourceMatcher base64UriMatcher() => (attributes, element) =>
14+
attributes["src"].startsWith("data:image") &&
15+
attributes["src"].contains("base64,");
16+
17+
ImageSourceMatcher networkSourceMatcher({
18+
List<String> schemas: const ["https", "http"],
19+
List<String> domains,
20+
String extension,
21+
}) =>
22+
(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"));
27+
};
28+
29+
typedef ImageRender = Widget Function(
30+
RenderContext context,
31+
Map<String, String> attributes,
32+
dom.Element element,
33+
);
34+
35+
ImageRender base64ImageRender() => (context, attributes, element) {
36+
final decodedImage =
37+
base64.decode(attributes["src"].split("base64,")[1].trim());
38+
precacheImage(
39+
MemoryImage(decodedImage),
40+
context.buildContext,
41+
onError: (exception, StackTrace stackTrace) {
42+
context.parser.onImageError?.call(exception, stackTrace);
43+
},
44+
);
45+
return Image.memory(
46+
decodedImage,
47+
frameBuilder: (ctx, child, frame, _) {
48+
if (frame == null) {
49+
return Text(attributes["alt"] ?? "",
50+
style: context.style.generateTextStyle());
51+
}
52+
return child;
53+
},
54+
);
55+
};
56+
57+
ImageRender networkImageRender({
58+
Map<String, String> headers,
59+
double width,
60+
double height,
61+
Widget Function(String) altWidget,
62+
}) =>
63+
(context, attributes, element) {
64+
precacheImage(
65+
NetworkImage(attributes["src"]),
66+
context.buildContext,
67+
onError: (exception, StackTrace stackTrace) {
68+
context.parser.onImageError?.call(exception, stackTrace);
69+
},
70+
);
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+
}
82+
return child;
83+
},
84+
);
85+
};
86+
87+
ImageRender svgNetworkImageRender() => (context, attributes, element) {
88+
return SvgPicture.network(
89+
attributes["src"],
90+
);
91+
};
92+
93+
final Map<ImageSourceMatcher, ImageRender> defaultImageRenders = {
94+
base64UriMatcher(): base64ImageRender(),
95+
networkSourceMatcher(extension: "svg"): svgNetworkImageRender(),
96+
networkSourceMatcher(): networkImageRender(),
97+
};

lib/src/replaced_element.dart

Lines changed: 7 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import 'dart:async';
2-
import 'dart:convert';
31
import 'dart:math';
42

53
import 'package:chewie/chewie.dart';
@@ -10,7 +8,6 @@ import 'package:flutter/material.dart';
108
import 'package:flutter/widgets.dart';
119
import 'package:flutter_html/html_parser.dart';
1210
import 'package:flutter_html/src/html_elements.dart';
13-
import 'package:flutter_html/src/utils.dart';
1411
import 'package:flutter_html/style.dart';
1512
import 'package:flutter_svg/flutter_svg.dart';
1613
import 'package:html/dom.dart' as dom;
@@ -81,115 +78,15 @@ class ImageContentElement extends ReplacedElement {
8178

8279
@override
8380
Widget toWidget(RenderContext context) {
84-
Widget imageWidget;
85-
if (src == null) {
86-
imageWidget = Text(alt ?? "", style: context.style.generateTextStyle());
87-
} else if (src.startsWith("data:image") && src.contains("base64,")) {
88-
final decodedImage = base64.decode(src.split("base64,")[1].trim());
89-
precacheImage(
90-
MemoryImage(decodedImage),
91-
context.buildContext,
92-
onError: (exception, StackTrace stackTrace) {
93-
context.parser.onImageError?.call(exception, stackTrace);
94-
},
95-
);
96-
imageWidget = Image.memory(
97-
decodedImage,
98-
frameBuilder: (ctx, child, frame, _) {
99-
if (frame == null) {
100-
return Text(alt ?? "", style: context.style.generateTextStyle());
101-
}
102-
return child;
103-
},
104-
);
105-
} else if (src.startsWith("asset:")) {
106-
final assetPath = src.replaceFirst('asset:', '');
107-
precacheImage(
108-
AssetImage(assetPath),
109-
context.buildContext,
110-
onError: (exception, StackTrace stackTrace) {
111-
context.parser.onImageError?.call(exception, stackTrace);
112-
},
113-
);
114-
imageWidget = Image.asset(
115-
assetPath,
116-
frameBuilder: (ctx, child, frame, _) {
117-
if (frame == null) {
118-
return Text(alt ?? "", style: context.style.generateTextStyle());
119-
}
120-
return child;
121-
},
122-
);
123-
} else if (src.endsWith(".svg")) {
124-
return SvgPicture.network(src);
125-
} else {
126-
precacheImage(
127-
NetworkImage(src),
128-
context.buildContext,
129-
onError: (exception, StackTrace stackTrace) {
130-
context.parser.onImageError?.call(exception, stackTrace);
131-
},
132-
);
133-
Completer<Size> completer = Completer();
134-
Image image = Image.network(src, frameBuilder: (ctx, child, frame, _) {
135-
if (frame == null) {
136-
completer.completeError("error");
137-
return child;
138-
} else {
139-
return child;
81+
for (final entry in context.parser.imageRenders.entries) {
82+
if (entry.key.call(attributes, element)) {
83+
final widget = entry.value.call(context, attributes, element);
84+
if (widget != null) {
85+
return widget;
14086
}
141-
});
142-
image.image.resolve(ImageConfiguration()).addListener(
143-
ImageStreamListener((ImageInfo image, bool synchronousCall) {
144-
var myImage = image.image;
145-
Size size =
146-
Size(myImage.width.toDouble(), myImage.height.toDouble());
147-
completer.complete(size);
148-
}, onError: (object, stacktrace) {
149-
completer.completeError(object);
150-
}),
151-
);
152-
imageWidget = FutureBuilder<Size>(
153-
future: completer.future,
154-
builder: (BuildContext buildContext, AsyncSnapshot<Size> snapshot) {
155-
if (snapshot.hasData) {
156-
return new Image.network(
157-
src,
158-
width: snapshot.data.width,
159-
frameBuilder: (ctx, child, frame, _) {
160-
if (frame == null) {
161-
return Text(alt ?? "",
162-
style: context.style.generateTextStyle());
163-
}
164-
return child;
165-
},
166-
);
167-
} else if (snapshot.hasError) {
168-
return Text(alt ?? "", style: context.style.generateTextStyle());
169-
} else {
170-
return new CircularProgressIndicator();
171-
}
172-
},
173-
);
87+
}
17488
}
175-
176-
return ContainerSpan(
177-
style: style,
178-
newContext: context,
179-
shrinkWrap: context.parser.shrinkWrap,
180-
child: RawGestureDetector(
181-
child: imageWidget,
182-
gestures: {
183-
MultipleTapGestureRecognizer: GestureRecognizerFactoryWithHandlers<
184-
MultipleTapGestureRecognizer>(
185-
() => MultipleTapGestureRecognizer(),
186-
(instance) {
187-
instance..onTap = () => context.parser.onImageTap?.call(src);
188-
},
189-
),
190-
},
191-
),
192-
);
89+
return Container();
19390
}
19491
}
19592

0 commit comments

Comments
 (0)