1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import '../base/common.dart';
import '../base/file_system.dart';
import '../base/utils.dart';
import '../convert.dart';
import '../device.dart';
import '../globals.dart' as globals;
import '../runner/flutter_command.dart';
import '../vmservice.dart';
const String _kOut = 'out';
const String _kType = 'type';
const String _kObservatoryUri = 'observatory-uri';
const String _kDeviceType = 'device';
const String _kSkiaType = 'skia';
const String _kRasterizerType = 'rasterizer';
class ScreenshotCommand extends FlutterCommand {
ScreenshotCommand() {
argParser.addOption(
_kOut,
abbr: 'o',
valueHelp: 'path/to/file',
help: 'Location to write the screenshot.',
);
argParser.addOption(
_kObservatoryUri,
valueHelp: 'URI',
help: 'The observatory URI to connect to.\n'
'This is required when --$_kType is "$_kSkiaType" or "$_kRasterizerType".\n'
'To find the observatory URI, use "flutter run" and look for'
'"An Observatory ... is available at" in the output.',
);
argParser.addOption(
_kType,
valueHelp: 'type',
help: 'The type of screenshot to retrieve.',
allowed: const <String>[_kDeviceType, _kSkiaType, _kRasterizerType],
allowedHelp: const <String, String>{
_kDeviceType: 'Delegate to the device\'s native screenshot capabilities. This '
'screenshots the entire screen currently being displayed (including content '
'not rendered by Flutter, like the device status bar).',
_kSkiaType: 'Render the Flutter app as a Skia picture. Requires --$_kObservatoryUri',
_kRasterizerType: 'Render the Flutter app using the rasterizer. Requires --$_kObservatoryUri',
},
defaultsTo: _kDeviceType,
);
}
@override
String get name => 'screenshot';
@override
String get description => 'Take a screenshot from a connected device.';
@override
final List<String> aliases = <String>['pic'];
Device device;
static void validateOptions(String screenshotType, Device device, String observatoryUri) {
switch (screenshotType) {
case _kDeviceType:
if (device == null) {
throwToolExit('Must have a connected device for screenshot type $screenshotType');
}
if (!device.supportsScreenshot) {
throwToolExit('Screenshot not supported for ${device.name}.');
}
break;
default:
if (observatoryUri == null) {
throwToolExit('Observatory URI must be specified for screenshot type $screenshotType');
}
}
}
@override
Future<FlutterCommandResult> verifyThenRunCommand(String commandPath) async {
device = await findTargetDevice();
validateOptions(stringArg(_kType), device, stringArg(_kObservatoryUri));
return super.verifyThenRunCommand(commandPath);
}
@override
Future<FlutterCommandResult> runCommand() async {
File outputFile;
if (argResults.wasParsed(_kOut)) {
outputFile = globals.fs.file(stringArg(_kOut));
}
switch (stringArg(_kType)) {
case _kDeviceType:
await runScreenshot(outputFile);
return null;
case _kSkiaType:
await runSkia(outputFile);
return null;
case _kRasterizerType:
await runRasterizer(outputFile);
return null;
}
return null;
}
Future<void> runScreenshot(File outputFile) async {
outputFile ??= getUniqueFile(globals.fs.currentDirectory, 'flutter', 'png');
try {
await device.takeScreenshot(outputFile);
} catch (error) {
throwToolExit('Error taking screenshot: $error');
}
_showOutputFileInfo(outputFile);
}
Future<void> runSkia(File outputFile) async {
final Map<String, dynamic> skp = await _invokeVmServiceRpc('_flutter.screenshotSkp');
outputFile ??= getUniqueFile(globals.fs.currentDirectory, 'flutter', 'skp');
final IOSink sink = outputFile.openWrite();
sink.add(base64.decode(skp['skp'] as String));
await sink.close();
_showOutputFileInfo(outputFile);
_ensureOutputIsNotJsonRpcError(outputFile);
}
Future<void> runRasterizer(File outputFile) async {
final Map<String, dynamic> response = await _invokeVmServiceRpc('_flutter.screenshot');
outputFile ??= getUniqueFile(globals.fs.currentDirectory, 'flutter', 'png');
final IOSink sink = outputFile.openWrite();
sink.add(base64.decode(response['screenshot'] as String));
await sink.close();
_showOutputFileInfo(outputFile);
_ensureOutputIsNotJsonRpcError(outputFile);
}
Future<Map<String, dynamic>> _invokeVmServiceRpc(String method) async {
final Uri observatoryUri = Uri.parse(stringArg(_kObservatoryUri));
final VMService vmService = await VMService.connect(observatoryUri);
return await vmService.vm.invokeRpcRaw(method);
}
void _ensureOutputIsNotJsonRpcError(File outputFile) {
if (outputFile.lengthSync() >= 1000) {
return;
}
final String content = outputFile.readAsStringSync(
encoding: const AsciiCodec(allowInvalid: true),
);
if (content.startsWith('{"jsonrpc":"2.0", "error"')) {
throwToolExit('It appears the output file contains an error message, not valid skia output.');
}
}
void _showOutputFileInfo(File outputFile) {
final int sizeKB = (outputFile.lengthSync()) ~/ 1024;
globals.printStatus('Screenshot written to ${globals.fs.path.relative(outputFile.path)} (${sizeKB}kB).');
}
}