Skip to content

Commit d1561e2

Browse files
author
Jonah Williams
authored
Allow processing SVGs in parallel with Isolate.run (#135)
1 parent ddf18dc commit d1561e2

File tree

5 files changed

+233
-53
lines changed

5 files changed

+233
-53
lines changed

packages/vector_graphics_compiler/bin/vector_graphics_compiler.dart

Lines changed: 68 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,9 @@
33
// found in the LICENSE file.
44

55
import 'dart:io';
6-
import 'dart:typed_data';
76

87
import 'package:args/args.dart';
9-
import 'package:vector_graphics_compiler/vector_graphics_compiler.dart';
8+
import 'package:vector_graphics_compiler/src/isolate_processor.dart';
109

1110
final ArgParser argParser = ArgParser()
1211
..addOption(
@@ -41,29 +40,39 @@ final ArgParser argParser = ArgParser()
4140
help: 'Allows for overdraw optimizer to be enabled or disabled',
4241
defaultsTo: true,
4342
)
44-
..addOption('input',
45-
abbr: 'i',
46-
help: 'The path to a file containing a single SVG',
47-
mandatory: true)
43+
..addOption(
44+
'input-dir',
45+
help: 'The path to a directory containing one or more SVGs. '
46+
'Only includes files that end with .svg. '
47+
'Cannot be combined with --input or --output.',
48+
)
49+
..addOption(
50+
'input',
51+
abbr: 'i',
52+
help: 'The path to a file containing a single SVG',
53+
)
54+
..addOption('concurrency',
55+
abbr: 'k',
56+
help: 'The maximum number of SVG processing isolates to spawn at once. '
57+
'If not provided, defaults to the number of cores.')
4858
..addOption(
4959
'output',
5060
abbr: 'o',
5161
help:
5262
'The path to a file where the resulting vector_graphic will be written.\n'
53-
'If not provided, defaults to <input-file>.vg',
63+
'If not provided, defaults to <input-file>.vec',
5464
);
5565

56-
void loadPathOpsIfNeeded(ArgResults results) {
57-
if (results['optimize-masks'] == true ||
58-
results['optimize-clips'] == true ||
59-
results['optimize-overdraw'] == true) {
60-
if (results.wasParsed('libpathops')) {
61-
initializeLibPathOps(results['libpathops'] as String);
62-
} else {
63-
if (!initializePathOpsFromFlutterCache()) {
64-
exit(1);
65-
}
66-
}
66+
void validateOptions(ArgResults results) {
67+
if (results.wasParsed('input-dir') &&
68+
(results.wasParsed('input') || results.wasParsed('output'))) {
69+
print(
70+
'--input-dir cannot be combined with --input and/or --output options.');
71+
exit(1);
72+
}
73+
if (!results.wasParsed('input') && !results.wasParsed('input-dir')) {
74+
print('One of --input or --input-dir must be specified.');
75+
exit(1);
6776
}
6877
}
6978

@@ -76,46 +85,53 @@ Future<void> main(List<String> args) async {
7685
print(argParser.usage);
7786
exit(1);
7887
}
88+
validateOptions(results);
7989

80-
if (results['tessellate'] == true) {
81-
if (results.wasParsed('libtessellator')) {
82-
initializeLibTesselator(results['libtessellator'] as String);
83-
} else {
84-
if (!initializeTessellatorFromFlutterCache()) {
85-
exit(1);
90+
final List<Pair> pairs = <Pair>[];
91+
if (results.wasParsed('input-dir')) {
92+
final Directory directory = Directory(results['input-dir'] as String);
93+
if (!directory.existsSync()) {
94+
print('input-dir ${directory.path} does not exist.');
95+
exit(1);
96+
}
97+
for (final File file
98+
in directory.listSync(recursive: true).whereType<File>()) {
99+
if (!file.path.endsWith('.svg')) {
100+
continue;
86101
}
102+
final String outputPath = '${file.path}.vec';
103+
pairs.add(Pair(file.path, outputPath));
87104
}
105+
} else {
106+
final String inputFilePath = results['input'] as String;
107+
final String outputFilePath =
108+
results['output'] as String? ?? '$inputFilePath.vec';
109+
pairs.add(Pair(inputFilePath, outputFilePath));
88110
}
89111

90-
loadPathOpsIfNeeded(results);
91-
92-
final String inputFilePath = results['input'] as String;
93-
final String xml = File(inputFilePath).readAsStringSync();
94-
final File outputFile =
95-
File(results['output'] as String? ?? '$inputFilePath.vg');
96-
97-
bool maskingOptimizerEnabled = true;
98-
bool clippingOptimizerEnabled = true;
99-
bool overdrawOptimizerEnabled = true;
100-
101-
if (results['optimize-masks'] == false) {
102-
maskingOptimizerEnabled = false;
103-
}
104-
105-
if (results['optimize-clips'] == false) {
106-
clippingOptimizerEnabled = false;
112+
final bool maskingOptimizerEnabled = results['optimize-masks'] == true;
113+
final bool clippingOptimizerEnabled = results['optimize-clips'] == true;
114+
final bool overdrawOptimizerEnabled = results['optimize-overdraw'] == true;
115+
final bool tessellate = results['tessellate'] == true;
116+
final int concurrency;
117+
if (results.wasParsed('concurrency')) {
118+
concurrency = int.parse(results['concurrency'] as String);
119+
} else {
120+
concurrency = Platform.numberOfProcessors;
107121
}
108122

109-
if (results['optimize-overdraw'] == false) {
110-
overdrawOptimizerEnabled = false;
123+
final IsolateProcessor processor = IsolateProcessor(
124+
results['libpathops'] as String?,
125+
results['libtessellator'] as String?,
126+
concurrency,
127+
);
128+
if (!await processor.process(
129+
pairs,
130+
maskingOptimizerEnabled: maskingOptimizerEnabled,
131+
clippingOptimizerEnabled: clippingOptimizerEnabled,
132+
overdrawOptimizerEnabled: overdrawOptimizerEnabled,
133+
tessellate: tessellate,
134+
)) {
135+
exit(1);
111136
}
112-
113-
final Uint8List bytes = await encodeSvg(
114-
xml: xml,
115-
debugName: args[0],
116-
enableMaskingOptimizer: maskingOptimizerEnabled,
117-
enableClippingOptimizer: clippingOptimizerEnabled,
118-
enableOverdrawOptimizer: overdrawOptimizerEnabled);
119-
120-
outputFile.writeAsBytesSync(bytes);
121137
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'dart:io';
6+
import 'dart:isolate';
7+
import 'dart:typed_data';
8+
9+
import 'package:pool/pool.dart';
10+
11+
import '../vector_graphics_compiler.dart';
12+
13+
/// The isolate processor distributes SVG compilation across multiple isolates.
14+
class IsolateProcessor {
15+
/// Create a new [IsolateProcessor].
16+
IsolateProcessor(this._libpathops, this._libtessellator, int concurrency)
17+
: _pool = Pool(concurrency);
18+
19+
final String? _libpathops;
20+
final String? _libtessellator;
21+
final Pool _pool;
22+
23+
int _total = 0;
24+
int _current = 0;
25+
26+
/// Process the provided input/output [Pair] objects into vector graphics.
27+
///
28+
/// Returns whether all requests were successful.
29+
Future<bool> process(
30+
List<Pair> pairs, {
31+
required bool maskingOptimizerEnabled,
32+
required bool clippingOptimizerEnabled,
33+
required bool overdrawOptimizerEnabled,
34+
required bool tessellate,
35+
}) async {
36+
_total = pairs.length;
37+
_current = 0;
38+
bool failure = false;
39+
await Future.wait(eagerError: true, <Future<void>>[
40+
for (Pair pair in pairs)
41+
_process(
42+
pair,
43+
maskingOptimizerEnabled: maskingOptimizerEnabled,
44+
clippingOptimizerEnabled: clippingOptimizerEnabled,
45+
overdrawOptimizerEnabled: overdrawOptimizerEnabled,
46+
tessellate: tessellate,
47+
libpathops: _libpathops,
48+
libtessellator: _libtessellator,
49+
).catchError((dynamic error, [StackTrace? stackTrace]) {
50+
failure = true;
51+
print('XXXXXXXXXXX ${pair.inputPath} XXXXXXXXXXXXX');
52+
print(error);
53+
print(stackTrace);
54+
}),
55+
]);
56+
if (failure) {
57+
print('Some targets failed.');
58+
}
59+
return !failure;
60+
}
61+
62+
static void _loadPathOps(String? libpathops) {
63+
if (libpathops != null && libpathops.isNotEmpty) {
64+
initializeLibPathOps(libpathops);
65+
} else if (!initializePathOpsFromFlutterCache()) {
66+
throw StateError('Could not find libpathops binary');
67+
}
68+
}
69+
70+
static void _loadTessellator(String? libtessellator) {
71+
if (libtessellator != null && libtessellator.isNotEmpty) {
72+
initializeLibTesselator(libtessellator);
73+
} else if (!initializeTessellatorFromFlutterCache()) {
74+
throw StateError('Could not find libtessellator binary');
75+
}
76+
}
77+
78+
Future<void> _process(
79+
Pair pair, {
80+
required bool maskingOptimizerEnabled,
81+
required bool clippingOptimizerEnabled,
82+
required bool overdrawOptimizerEnabled,
83+
required bool tessellate,
84+
required String? libpathops,
85+
required String? libtessellator,
86+
}) async {
87+
PoolResource? resource;
88+
try {
89+
resource = await _pool.request();
90+
await Isolate.run(() async {
91+
if (maskingOptimizerEnabled ||
92+
clippingOptimizerEnabled ||
93+
overdrawOptimizerEnabled) {
94+
_loadPathOps(libpathops);
95+
}
96+
if (tessellate) {
97+
_loadTessellator(libtessellator);
98+
}
99+
100+
final Uint8List bytes = await encodeSvg(
101+
xml: File(pair.inputPath).readAsStringSync(),
102+
debugName: pair.inputPath,
103+
enableMaskingOptimizer: maskingOptimizerEnabled,
104+
enableClippingOptimizer: clippingOptimizerEnabled,
105+
enableOverdrawOptimizer: overdrawOptimizerEnabled,
106+
);
107+
File(pair.outputPath).writeAsBytesSync(bytes);
108+
});
109+
_current++;
110+
print('Progress: $_current/$_total');
111+
} finally {
112+
resource?.release();
113+
}
114+
}
115+
}
116+
117+
/// A combination of an input file and its output file.
118+
class Pair {
119+
/// Create a new [Pair].
120+
const Pair(this.inputPath, this.outputPath);
121+
122+
/// The path the SVG should be read from.
123+
final String inputPath;
124+
125+
/// The path the vector graphic will be written to.
126+
final String outputPath;
127+
}

packages/vector_graphics_compiler/pubspec.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,15 @@ executables:
77
vector_graphics_compiler:
88

99
environment:
10-
sdk: '>=2.17.0 <3.0.0'
10+
sdk: '>=2.19.0-214.0.dev <3.0.0'
1111

1212
dependencies:
1313
args: ^2.3.0
1414
meta: ^1.7.0
1515
path_parsing: ^1.0.1
1616
xml: ^6.0.1
1717
vector_graphics_codec: 0.0.2
18+
pool: ^1.5.0
1819

1920
dev_dependencies:
2021
flutter_lints: ^1.0.0
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'dart:io';
6+
7+
import 'package:test/test.dart';
8+
import 'package:vector_graphics_compiler/src/isolate_processor.dart';
9+
10+
void main() {
11+
test('Can run with isolate processor', () async {
12+
final File output = File('test_data/example.vec');
13+
try {
14+
final IsolateProcessor processor = IsolateProcessor(null, null, 4);
15+
final bool result = await processor.process(
16+
<Pair>[
17+
Pair('test_data/example.svg', output.path),
18+
],
19+
maskingOptimizerEnabled: false,
20+
clippingOptimizerEnabled: false,
21+
overdrawOptimizerEnabled: false,
22+
tessellate: false,
23+
);
24+
expect(result, isTrue);
25+
expect(output.existsSync(), isTrue);
26+
} finally {
27+
if (output.existsSync()) {
28+
output.deleteSync();
29+
}
30+
}
31+
});
32+
}
Lines changed: 4 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)