From c7ed0fe3c6410cb4bcc50d963b459c0a32ca10c6 Mon Sep 17 00:00:00 2001 From: Dan Field Date: Mon, 23 Dec 2019 15:17:54 -0800 Subject: [PATCH 01/23] Font subsetting tool --- BUILD.gn | 4 ++ tools/font-subset/BUILD.gn | 23 ++++++ tools/font-subset/hb_wrappers.h | 30 ++++++++ tools/font-subset/main.cc | 119 ++++++++++++++++++++++++++++++++ 4 files changed, 176 insertions(+) create mode 100644 tools/font-subset/BUILD.gn create mode 100644 tools/font-subset/hb_wrappers.h create mode 100644 tools/font-subset/main.cc diff --git a/BUILD.gn b/BUILD.gn index 4cb8037d3a658..ce3a55c500b22 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -31,6 +31,10 @@ group("flutter") { "$flutter_root/sky", ] + if (current_toolchain == host_toolchain) { + public_deps += [ "$flutter_root/tools/font-subset" ] + } + if (current_toolchain == host_toolchain) { public_deps += [ "$flutter_root/shell/testing" ] } diff --git a/tools/font-subset/BUILD.gn b/tools/font-subset/BUILD.gn new file mode 100644 index 0000000000000..5cf8d4cf1d7b2 --- /dev/null +++ b/tools/font-subset/BUILD.gn @@ -0,0 +1,23 @@ +# Copyright 2013 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. + +executable("font-subset") { + sources = [ + "hb_wrappers.h", + "main.cc", + ] + + deps = [ + "//third_party/harfbuzz" + ] + + libs = [] + if (is_mac) { + libs += [ + "Foundation.framework", + "CoreGraphics.framework", + "CoreText.framework", + ] + } +} diff --git a/tools/font-subset/hb_wrappers.h b/tools/font-subset/hb_wrappers.h new file mode 100644 index 0000000000000..1028a1b683304 --- /dev/null +++ b/tools/font-subset/hb_wrappers.h @@ -0,0 +1,30 @@ +// Copyright 2013 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. + +#ifndef HB_WRAPPERS_H_ +#define HB_WRAPPERS_H_ + +#include + +namespace HarfbuzzWrappers { +struct hb_blob_deleter { + void operator()(hb_blob_t* ptr) { hb_blob_destroy(ptr); } +}; + +struct hb_face_deleter { + void operator()(hb_face_t* ptr) { hb_face_destroy(ptr); } +}; + +struct hb_subset_input_deleter { + void operator()(hb_subset_input_t* ptr) { hb_subset_input_destroy(ptr); } +}; + +using HbBlobPtr = std::unique_ptr; +using HbFacePtr = std::unique_ptr; +using HbSubsetInputPtr = + std::unique_ptr; + +}; // namespace HarfbuzzWrappers + +#endif // HB_WRAPPERS_H_s diff --git a/tools/font-subset/main.cc b/tools/font-subset/main.cc new file mode 100644 index 0000000000000..9628481ff124e --- /dev/null +++ b/tools/font-subset/main.cc @@ -0,0 +1,119 @@ +// Copyright 2013 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. + +#include +#include +#include +#include +#include +#include +#include + +#include "hb_wrappers.h" + +void AddCodepoints(hb_subset_input_t* input, + std::set codepoints) { + hb_set_t* input_codepoints = hb_subset_input_unicode_set(input); + for (hb_codepoint_t codepoint : codepoints) { + hb_set_add(input_codepoints, codepoint); + } +} + +hb_codepoint_t ParseCodepoint(const char* arg) { + unsigned long value = 0; + // Check for \u123, u123, otherwise let strtoul work it out. + if (arg[0] == 'u') { + value = strtoul(arg + 1, nullptr, 16); + } else { + value = strtoul(arg, nullptr, 0); + } + + if (value == 0 || value > std::numeric_limits::max()) { + std::cerr << "The value '" << arg + << "' could not be parsed as a valid unicode codepoint; ignoring." + << std::endl; + return 0; + } + return value; +} + +void Usage() { + std::cout << "Usage:" << std::endl; + std::cout << "font-subset [CODEPOINTS]" << std::endl; + std::cout << std::endl; + std::cout << "The output.ttf file will be overwritten if it exists already " + "and the subsetting operation succeeds." + << std::endl; + std::cout << "At least one code point must be specified. The code points " + "should be separated by spaces, and must be input as decimal " + "numbers (123), hexidecimal numbers (0x7B), or unicode " + "hexidecimal characters (\\u7B)." + << std::endl; + std::cout + << "This program will de-duplicate codepoints if the same codepoint is " + "specified multiple times, e.g. '123 123' will be treated as '123'." + << std::endl; +} + +int main(int argc, char** argv) { + if (argc <= 3) { + Usage(); + return -1; + } + std::string output_file_path(argv[1]); + std::string input_file_path(argv[2]); + std::cout << "Using output file: " << output_file_path << std::endl; + std::cout << "Using source file: " << input_file_path << std::endl; + + std::set codepoints; + for (int i = 3; i < argc; i++) { + auto codepoint = ParseCodepoint(argv[i]); + if (codepoint) { + codepoints.insert(codepoint); + } + } + + HarfbuzzWrappers::HbBlobPtr font_blob( + hb_blob_create_from_file(input_file_path.c_str())); + if (!hb_blob_get_length(font_blob.get())) { + std::cerr << "Failed to load input font " << input_file_path + << "; aborting." << std::endl; + return -1; + } + + HarfbuzzWrappers::HbFacePtr font_face(hb_face_create(font_blob.get(), 0)); + if (font_face.get() == hb_face_get_empty()) { + std::cerr << "Failed to load input font face " << input_file_path + << "; aborting." << std::endl; + return -1; + } + + HarfbuzzWrappers::HbSubsetInputPtr input(hb_subset_input_create_or_fail()); + AddCodepoints(input.get(), std::move(codepoints)); + HarfbuzzWrappers::HbFacePtr new_face(hb_subset(font_face.get(), input.get())); + + if (new_face.get() == hb_face_get_empty()) { + std::cerr << "Failed to subset font; aborting." << std::endl; + return -1; + } + + HarfbuzzWrappers::HbBlobPtr result(hb_face_reference_blob(new_face.get())); + if (!hb_blob_get_length(result.get())) { + std::cerr << "Failed get new font bytes; aborting" << std::endl; + return -1; + } + + unsigned int data_length; + const char* data = hb_blob_get_data(result.get(), &data_length); + + std::ofstream output_font_file; + output_font_file.open(output_file_path, + std::ios::out | std::ios::trunc | std::ios::binary); + output_font_file.write(data, data_length); + output_font_file.close(); + + std::cout << "Wrote " << data_length << " bytes to " << output_file_path + << std::endl; + return 0; +} From ca586810acd50ee6f88f396e2599784ba157a545 Mon Sep 17 00:00:00 2001 From: Dan Field Date: Mon, 23 Dec 2019 20:28:44 -0800 Subject: [PATCH 02/23] check for invalid codes --- tools/font-subset/hb_wrappers.h | 5 +++++ tools/font-subset/main.cc | 34 ++++++++++++++++----------------- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/tools/font-subset/hb_wrappers.h b/tools/font-subset/hb_wrappers.h index 1028a1b683304..9638bebe610c1 100644 --- a/tools/font-subset/hb_wrappers.h +++ b/tools/font-subset/hb_wrappers.h @@ -20,10 +20,15 @@ struct hb_subset_input_deleter { void operator()(hb_subset_input_t* ptr) { hb_subset_input_destroy(ptr); } }; +struct hb_set_deleter { + void operator()(hb_set_t* ptr) { hb_set_destroy(ptr); } +}; + using HbBlobPtr = std::unique_ptr; using HbFacePtr = std::unique_ptr; using HbSubsetInputPtr = std::unique_ptr; +using HbSetPtr = std::unique_ptr; }; // namespace HarfbuzzWrappers diff --git a/tools/font-subset/main.cc b/tools/font-subset/main.cc index 9628481ff124e..4c9af4127da31 100644 --- a/tools/font-subset/main.cc +++ b/tools/font-subset/main.cc @@ -3,7 +3,6 @@ // found in the LICENSE file. #include -#include #include #include #include @@ -12,14 +11,6 @@ #include "hb_wrappers.h" -void AddCodepoints(hb_subset_input_t* input, - std::set codepoints) { - hb_set_t* input_codepoints = hb_subset_input_unicode_set(input); - for (hb_codepoint_t codepoint : codepoints) { - hb_set_add(input_codepoints, codepoint); - } -} - hb_codepoint_t ParseCodepoint(const char* arg) { unsigned long value = 0; // Check for \u123, u123, otherwise let strtoul work it out. @@ -66,14 +57,6 @@ int main(int argc, char** argv) { std::cout << "Using output file: " << output_file_path << std::endl; std::cout << "Using source file: " << input_file_path << std::endl; - std::set codepoints; - for (int i = 3; i < argc; i++) { - auto codepoint = ParseCodepoint(argv[i]); - if (codepoint) { - codepoints.insert(codepoint); - } - } - HarfbuzzWrappers::HbBlobPtr font_blob( hb_blob_create_from_file(input_file_path.c_str())); if (!hb_blob_get_length(font_blob.get())) { @@ -90,7 +73,22 @@ int main(int argc, char** argv) { } HarfbuzzWrappers::HbSubsetInputPtr input(hb_subset_input_create_or_fail()); - AddCodepoints(input.get(), std::move(codepoints)); + { + hb_set_t* desired_codepoints = hb_subset_input_unicode_set(input.get()); + HarfbuzzWrappers::HbSetPtr actual_codepoints(hb_set_create()); + hb_face_collect_unicodes(font_face.get(), actual_codepoints.get()); + for (int i = 3; i < argc; i++) { + auto codepoint = ParseCodepoint(argv[i]); + if (codepoint) { + if (!hb_set_has(actual_codepoints.get(), codepoint)) { + std::cerr << "Codepoint " << argv[i] + << " not found in font, aborting." << std::endl; + return -1; + } + hb_set_add(desired_codepoints, codepoint); + } + } + } HarfbuzzWrappers::HbFacePtr new_face(hb_subset(font_face.get(), input.get())); if (new_face.get() == hb_face_get_empty()) { From 59859245219edd58e11800096e13af9a47ec0c0e Mon Sep 17 00:00:00 2001 From: Dan Field Date: Fri, 27 Dec 2019 20:16:44 -0800 Subject: [PATCH 03/23] tests --- testing/run_tests.py | 6 +- tools/font-subset/.gitignore | 1 + tools/font-subset/fixtures/1.ttf | Bin 0 -> 1144 bytes tools/font-subset/fixtures/2.ttf | Bin 0 -> 1252 bytes tools/font-subset/fixtures/3.ttf | Bin 0 -> 1252 bytes .../fixtures/MaterialIcons-Regular.ttf | Bin 0 -> 134640 bytes tools/font-subset/main.cc | 9 ++- tools/font-subset/test.py | 74 ++++++++++++++++++ 8 files changed, 85 insertions(+), 5 deletions(-) create mode 100644 tools/font-subset/.gitignore create mode 100644 tools/font-subset/fixtures/1.ttf create mode 100644 tools/font-subset/fixtures/2.ttf create mode 100644 tools/font-subset/fixtures/3.ttf create mode 100644 tools/font-subset/fixtures/MaterialIcons-Regular.ttf create mode 100755 tools/font-subset/test.py diff --git a/testing/run_tests.py b/testing/run_tests.py index 0bb6a88d5b8d5..f17207d3a5a10 100755 --- a/testing/run_tests.py +++ b/testing/run_tests.py @@ -20,6 +20,7 @@ fonts_dir = os.path.join(buildroot_dir, 'flutter', 'third_party', 'txt', 'third_party', 'fonts') roboto_font_path = os.path.join(fonts_dir, 'Roboto-Regular.ttf') dart_tests_dir = os.path.join(buildroot_dir, 'flutter', 'testing', 'dart',) +font_subset_dir = os.path.join(buildroot_dir, 'flutter', 'tools', 'font-subset') fml_unittests_filter = '--gtest_filter=-*TimeSensitiveTest*:*GpuThreadMerger*' @@ -326,7 +327,7 @@ def main(): args = parser.parse_args() if args.type == 'all': - types = ['engine', 'dart', 'benchmarks', 'java'] + types = ['engine', 'dart', 'benchmarks', 'java', 'font-subset'] else: types = args.type.split(',') @@ -355,6 +356,9 @@ def main(): if 'benchmarks' in types and not IsWindows(): RunEngineBenchmarks(build_dir, engine_filter) + if 'font-subset' in types: + RunCmd(['python', 'test.py'], cwd=font_subset_dir) + if __name__ == '__main__': sys.exit(main()) diff --git a/tools/font-subset/.gitignore b/tools/font-subset/.gitignore new file mode 100644 index 0000000000000..6cf8af2628c07 --- /dev/null +++ b/tools/font-subset/.gitignore @@ -0,0 +1 @@ +gen/*.ttf \ No newline at end of file diff --git a/tools/font-subset/fixtures/1.ttf b/tools/font-subset/fixtures/1.ttf new file mode 100644 index 0000000000000000000000000000000000000000..5bf1856f56cbcb81b2d562fd66e60aaed45e7dd5 GIT binary patch literal 1144 zcmaJ=-%C?r7=FI9vrRL!13NCw@Z_@a$251^D%r(}EE9^DWk>|=F!w{)+;Gb@Dug1u z>?-Q6i|)Ef>9W8-z>6TH8zZ|+5Q36!Wb66Pwh)89?fc&Md7kgb^S)CE00*%M7p!a7 z2SZyUyJ<3Oobg1?F5u{^?||qho=nV@3}|A6_zAI^wu^;Ig(o4Fh#S(`h1A#K-wN?Z z;?+#jo@(@bNf5svj$}xvLZPlaBR-zVmF5)-8u2?~Pd1;h5l0XAOWl*R=L@)vZ`9ut z8#8t;dE{sKA@K(H+%M#drOH!;Xk}9BP!)O7P3?+Ekr}PU9khXt6}aiOJ`UBBdR*Le zZMtZVqq;)&ralbxF_q`#Jj+$8G9F;;U=QW!a8?=sHn~qu<|@6>BtH(%o>8e(I?K)- zsP+_synMI^ajx=q;rjezI_AIA65T;#)zhb6YZs}2@iI^;D^OIzhPq`&_7g9~1f6*? zuq-ebUGb{e4lR4!8_^Y3xE;c>OVVyhuXsITH@YGuZ)&D?($oUl(!EqlOqAbf!sI;` zbg?Sd(S%NP1Kr_3IG_h~Q#ZqYzkV!yCVV;~XQ)-zTU&Ln>GO1*5EE9fW%WJ^mKTH8 zkgvlxWPP~N*44JvXIcElf_{JSff;LRY4-V=Tbg3#y4K$A)LpIg?D^_$MFBKJw%z!1Hi;u^*!hbcL6mM4-_Lyl@- znEC!Uj6JVZcSQKwInI!CV`SgU|2uzyZx^uO_)jClTIAbRUj|00=IJO)PyJNJ=sZDZ l2F{bqGLEyJ>batWlB`^IV~*>T(|!4u&rx|S|3Cj7e*kLNtvdh! literal 0 HcmV?d00001 diff --git a/tools/font-subset/fixtures/2.ttf b/tools/font-subset/fixtures/2.ttf new file mode 100644 index 0000000000000000000000000000000000000000..4bb2c2c44d9ae32913cb8c5ee97b7acfd1e77ee8 GIT binary patch literal 1252 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 literal 0 HcmV?d00001 diff --git a/tools/font-subset/main.cc b/tools/font-subset/main.cc index 4c9af4127da31..49504bef7e0db 100644 --- a/tools/font-subset/main.cc +++ b/tools/font-subset/main.cc @@ -16,15 +16,16 @@ hb_codepoint_t ParseCodepoint(const char* arg) { // Check for \u123, u123, otherwise let strtoul work it out. if (arg[0] == 'u') { value = strtoul(arg + 1, nullptr, 16); + } else if (arg[0] == '\\' && arg[1] == 'u') { + value = strtoul(arg + 2, nullptr, 16); } else { value = strtoul(arg, nullptr, 0); } - if (value == 0 || value > std::numeric_limits::max()) { - std::cerr << "The value '" << arg - << "' could not be parsed as a valid unicode codepoint; ignoring." + std::cerr << "The value '" << arg << "' (" << value + << ") could not be parsed as a valid unicode codepoint; aborting." << std::endl; - return 0; + exit(-1); } return value; } diff --git a/tools/font-subset/test.py b/tools/font-subset/test.py new file mode 100755 index 0000000000000..cffffd0cdacbc --- /dev/null +++ b/tools/font-subset/test.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python +# +# Copyright 2013 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. + +''' +Tests for font-subset +''' + +import filecmp +import os +import subprocess +import sys + +SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) +SRC_DIR = os.path.join(SCRIPT_DIR, '..', '..', '..') +MATERIAL_TTF = os.path.join(SCRIPT_DIR, 'fixtures', 'MaterialIcons-Regular.ttf') +FONT_SUBSET = 'out/host_debug/font-subset' + +COMPARE_TESTS = ( + (True, '1.ttf', MATERIAL_TTF, [r'57347']), + (True, '1.ttf', MATERIAL_TTF, [r'0xE003']), + (True, '1.ttf', MATERIAL_TTF, [r'\uE003']), + (False, '1.ttf', MATERIAL_TTF, [r'57348']), + (True, '2.ttf', MATERIAL_TTF, [r'0xE003', r'0xE004']), + (True, '3.ttf', MATERIAL_TTF, [r'0xE003', r'0xE004', r'57347',]), +) + + +FAIL_TESTS = [ + [FONT_SUBSET, 'output.ttf', 'does-not-exist.ttf', '1'], # non-existant input font + [FONT_SUBSET, 'output.ttf', MATERIAL_TTF, '0xFFFFFFFF'], # Value too big. + [FONT_SUBSET, 'output.ttf', MATERIAL_TTF, '-1'], # invalid value + [FONT_SUBSET, 'output.ttf', MATERIAL_TTF, 'foo'], # no valid values + [FONT_SUBSET, 'output.ttf', MATERIAL_TTF, r'0xE003', r'0x12', r'0xE004',] # codepoint not in font +] + + +def main(): + subprocess.check_output(['flutter/tools/gn'], cwd=SRC_DIR) + subprocess.check_output(['autoninja', '-C', 'out/host_debug', 'font-subset'], + cwd=SRC_DIR) + failures = 0 + for should_pass, golden_font, input_font, codepoints in COMPARE_TESTS: + gen_ttf = os.path.join(SCRIPT_DIR, 'gen', golden_font) + golden_ttf = os.path.join(SCRIPT_DIR, 'fixtures', golden_font) + cmd = [FONT_SUBSET, gen_ttf, input_font] + codepoints + print('Running test %s...' % cmd) + subprocess.check_output(cmd, cwd=SRC_DIR) + cmp = filecmp.cmp(gen_ttf, golden_ttf, shallow=False) + if (should_pass and not cmp) or (not should_pass and cmp): + print('Test case %s failed.' % cmd) + failures += 1 + + devnull = open(os.devnull, 'w') + for cmd in FAIL_TESTS: + print('Running test %s...' % cmd) + if subprocess.call(cmd, cwd=SRC_DIR, stdout=devnull, stderr=devnull) == 0: + print('Command %s passed, expected failure.' % cmd) + failures += 1 + devnull.close() + + if failures > 1: + print('%s tests failed.' % failures) + return 1 + + print('All tests passed') + return 0 + + +if __name__ == '__main__': + sys.exit(main()) + From 1f182b0e33b614744cd78ba72f5fa26c5071aa49 Mon Sep 17 00:00:00 2001 From: Dan Field Date: Fri, 27 Dec 2019 22:21:57 -0800 Subject: [PATCH 04/23] windows, format --- tools/font-subset/BUILD.gn | 2 +- tools/font-subset/main.cc | 1 + tools/font-subset/test.py | 3 ++- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/tools/font-subset/BUILD.gn b/tools/font-subset/BUILD.gn index 5cf8d4cf1d7b2..9c2625833af3d 100644 --- a/tools/font-subset/BUILD.gn +++ b/tools/font-subset/BUILD.gn @@ -9,7 +9,7 @@ executable("font-subset") { ] deps = [ - "//third_party/harfbuzz" + "//third_party/harfbuzz", ] libs = [] diff --git a/tools/font-subset/main.cc b/tools/font-subset/main.cc index 49504bef7e0db..e364dfcb28a37 100644 --- a/tools/font-subset/main.cc +++ b/tools/font-subset/main.cc @@ -8,6 +8,7 @@ #include #include #include +#include #include "hb_wrappers.h" diff --git a/tools/font-subset/test.py b/tools/font-subset/test.py index cffffd0cdacbc..9a455fabbe21a 100755 --- a/tools/font-subset/test.py +++ b/tools/font-subset/test.py @@ -16,7 +16,8 @@ SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) SRC_DIR = os.path.join(SCRIPT_DIR, '..', '..', '..') MATERIAL_TTF = os.path.join(SCRIPT_DIR, 'fixtures', 'MaterialIcons-Regular.ttf') -FONT_SUBSET = 'out/host_debug/font-subset' +EXE = '' if not sys.platform.startswith(('cygwin', 'win')) else '.exe' +FONT_SUBSET = 'out/host_debug/font-subset' + EXE COMPARE_TESTS = ( (True, '1.ttf', MATERIAL_TTF, [r'57347']), From 690cc78abbdb7a3c16d9e08f99478817e854f113 Mon Sep 17 00:00:00 2001 From: Dan Field Date: Fri, 27 Dec 2019 22:38:15 -0800 Subject: [PATCH 05/23] run font-subset with engine tests --- testing/run_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/run_tests.py b/testing/run_tests.py index f17207d3a5a10..0827f387d2822 100755 --- a/testing/run_tests.py +++ b/testing/run_tests.py @@ -356,7 +356,7 @@ def main(): if 'benchmarks' in types and not IsWindows(): RunEngineBenchmarks(build_dir, engine_filter) - if 'font-subset' in types: + if 'engine' in types or 'font-subset' in types: RunCmd(['python', 'test.py'], cwd=font_subset_dir) From 033a033b2652ff7f4bf680b85045f96ac8092f61 Mon Sep 17 00:00:00 2001 From: Dan Field Date: Sat, 28 Dec 2019 06:41:04 -0800 Subject: [PATCH 06/23] windows --- tools/font-subset/test.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tools/font-subset/test.py b/tools/font-subset/test.py index 9a455fabbe21a..de6810ef44c8c 100755 --- a/tools/font-subset/test.py +++ b/tools/font-subset/test.py @@ -16,7 +16,9 @@ SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) SRC_DIR = os.path.join(SCRIPT_DIR, '..', '..', '..') MATERIAL_TTF = os.path.join(SCRIPT_DIR, 'fixtures', 'MaterialIcons-Regular.ttf') -EXE = '' if not sys.platform.startswith(('cygwin', 'win')) else '.exe' +IS_WINDOWS = sys.platform.startswith(('cygwin', 'win')) +EXE = '.exe' if IS_WINDOWS else '' +BAT = '.bat' if IS_WINDOWS else '' FONT_SUBSET = 'out/host_debug/font-subset' + EXE COMPARE_TESTS = ( @@ -39,8 +41,8 @@ def main(): - subprocess.check_output(['flutter/tools/gn'], cwd=SRC_DIR) - subprocess.check_output(['autoninja', '-C', 'out/host_debug', 'font-subset'], + subprocess.check_output(['python', 'flutter/tools/gn'], cwd=SRC_DIR) + subprocess.check_output(['autoninja' + BAT, '-C', 'out/host_debug', 'font-subset'], cwd=SRC_DIR) failures = 0 for should_pass, golden_font, input_font, codepoints in COMPARE_TESTS: From 373b6e81a9a606e4052b86f62e2e5ea8c03b2a21 Mon Sep 17 00:00:00 2001 From: Dan Field Date: Sat, 28 Dec 2019 07:21:36 -0800 Subject: [PATCH 07/23] .. --- tools/font-subset/test.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/tools/font-subset/test.py b/tools/font-subset/test.py index de6810ef44c8c..5d8b3f0ecf2f1 100755 --- a/tools/font-subset/test.py +++ b/tools/font-subset/test.py @@ -30,7 +30,6 @@ (True, '3.ttf', MATERIAL_TTF, [r'0xE003', r'0xE004', r'57347',]), ) - FAIL_TESTS = [ [FONT_SUBSET, 'output.ttf', 'does-not-exist.ttf', '1'], # non-existant input font [FONT_SUBSET, 'output.ttf', MATERIAL_TTF, '0xFFFFFFFF'], # Value too big. @@ -39,18 +38,24 @@ [FONT_SUBSET, 'output.ttf', MATERIAL_TTF, r'0xE003', r'0x12', r'0xE004',] # codepoint not in font ] +def RunCmd(cmd, **kwargs): + try: + print(subprocess.check_output(cmd, **kwargs)) + except subprocess.CalledProcessError as cpe: + print(cpe.output) + raise cpe + def main(): - subprocess.check_output(['python', 'flutter/tools/gn'], cwd=SRC_DIR) - subprocess.check_output(['autoninja' + BAT, '-C', 'out/host_debug', 'font-subset'], - cwd=SRC_DIR) + RunCmd(['python', 'flutter/tools/gn'], cwd=SRC_DIR) + RunCmd(['autoninja' + BAT, '-C', 'out/host_debug', 'font-subset'], cwd=SRC_DIR) failures = 0 for should_pass, golden_font, input_font, codepoints in COMPARE_TESTS: gen_ttf = os.path.join(SCRIPT_DIR, 'gen', golden_font) golden_ttf = os.path.join(SCRIPT_DIR, 'fixtures', golden_font) cmd = [FONT_SUBSET, gen_ttf, input_font] + codepoints print('Running test %s...' % cmd) - subprocess.check_output(cmd, cwd=SRC_DIR) + RunCmd(cmd, cwd=SRC_DIR) cmp = filecmp.cmp(gen_ttf, golden_ttf, shallow=False) if (should_pass and not cmp) or (not should_pass and cmp): print('Test case %s failed.' % cmd) From 80340bf17535c9d404c205240f112c14e535fba1 Mon Sep 17 00:00:00 2001 From: Dan Field Date: Sat, 28 Dec 2019 13:03:49 -0800 Subject: [PATCH 08/23] .. --- tools/font-subset/test.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tools/font-subset/test.py b/tools/font-subset/test.py index 5d8b3f0ecf2f1..610457de2086c 100755 --- a/tools/font-subset/test.py +++ b/tools/font-subset/test.py @@ -19,7 +19,7 @@ IS_WINDOWS = sys.platform.startswith(('cygwin', 'win')) EXE = '.exe' if IS_WINDOWS else '' BAT = '.bat' if IS_WINDOWS else '' -FONT_SUBSET = 'out/host_debug/font-subset' + EXE +FONT_SUBSET = os.path.join(SRC_DIR, 'out', 'host_debug', 'font-subset' + EXE) COMPARE_TESTS = ( (True, '1.ttf', MATERIAL_TTF, [r'57347']), @@ -47,6 +47,8 @@ def RunCmd(cmd, **kwargs): def main(): + if os.environ['GOMA_DIR']: + RunCmd(['python', os.path.join(os.environ['GOMA_DIR'], 'goma_ctl.py'), 'start']) RunCmd(['python', 'flutter/tools/gn'], cwd=SRC_DIR) RunCmd(['autoninja' + BAT, '-C', 'out/host_debug', 'font-subset'], cwd=SRC_DIR) failures = 0 From aa342f65f46ccbaa896c043f80fefb74a6410dfd Mon Sep 17 00:00:00 2001 From: Dan Field Date: Sat, 28 Dec 2019 13:41:42 -0800 Subject: [PATCH 09/23] no build --- tools/font-subset/test.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tools/font-subset/test.py b/tools/font-subset/test.py index 610457de2086c..b7345990b13cc 100755 --- a/tools/font-subset/test.py +++ b/tools/font-subset/test.py @@ -47,10 +47,6 @@ def RunCmd(cmd, **kwargs): def main(): - if os.environ['GOMA_DIR']: - RunCmd(['python', os.path.join(os.environ['GOMA_DIR'], 'goma_ctl.py'), 'start']) - RunCmd(['python', 'flutter/tools/gn'], cwd=SRC_DIR) - RunCmd(['autoninja' + BAT, '-C', 'out/host_debug', 'font-subset'], cwd=SRC_DIR) failures = 0 for should_pass, golden_font, input_font, codepoints in COMPARE_TESTS: gen_ttf = os.path.join(SCRIPT_DIR, 'gen', golden_font) From bb6f843391d46d255845b6b6e8b036e6a03b2111 Mon Sep 17 00:00:00 2001 From: Dan Field Date: Sat, 28 Dec 2019 13:55:57 -0800 Subject: [PATCH 10/23] flush --- tools/font-subset/main.cc | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/font-subset/main.cc b/tools/font-subset/main.cc index e364dfcb28a37..f801bdcc5dc6b 100644 --- a/tools/font-subset/main.cc +++ b/tools/font-subset/main.cc @@ -111,6 +111,7 @@ int main(int argc, char** argv) { output_font_file.open(output_file_path, std::ios::out | std::ios::trunc | std::ios::binary); output_font_file.write(data, data_length); + output_font_file.flush(); output_font_file.close(); std::cout << "Wrote " << data_length << " bytes to " << output_file_path From 1cc4fcf288bd4cbeb57ec2a52d4d71ce40ab3a17 Mon Sep 17 00:00:00 2001 From: Dan Field Date: Sun, 29 Dec 2019 10:23:44 -0800 Subject: [PATCH 11/23] .. --- testing/run_tests.py | 5 ++++- tools/font-subset/test.py | 6 ++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/testing/run_tests.py b/testing/run_tests.py index 0827f387d2822..2119cf8bc8a18 100755 --- a/testing/run_tests.py +++ b/testing/run_tests.py @@ -22,6 +22,9 @@ dart_tests_dir = os.path.join(buildroot_dir, 'flutter', 'testing', 'dart',) font_subset_dir = os.path.join(buildroot_dir, 'flutter', 'tools', 'font-subset') +BAT = '.bat' if IsWindows() else '' +VPYTHON = 'vpython' + BAT + fml_unittests_filter = '--gtest_filter=-*TimeSensitiveTest*:*GpuThreadMerger*' def RunCmd(cmd, **kwargs): @@ -357,7 +360,7 @@ def main(): RunEngineBenchmarks(build_dir, engine_filter) if 'engine' in types or 'font-subset' in types: - RunCmd(['python', 'test.py'], cwd=font_subset_dir) + RunCmd([VPYTHON, 'test.py'], cwd=font_subset_dir) if __name__ == '__main__': diff --git a/tools/font-subset/test.py b/tools/font-subset/test.py index b7345990b13cc..24d1428a93059 100755 --- a/tools/font-subset/test.py +++ b/tools/font-subset/test.py @@ -19,6 +19,8 @@ IS_WINDOWS = sys.platform.startswith(('cygwin', 'win')) EXE = '.exe' if IS_WINDOWS else '' BAT = '.bat' if IS_WINDOWS else '' +VPYTHON = 'vpython' + BAT +AUTONINJA = 'autoninja' + BAT FONT_SUBSET = os.path.join(SRC_DIR, 'out', 'host_debug', 'font-subset' + EXE) COMPARE_TESTS = ( @@ -47,6 +49,10 @@ def RunCmd(cmd, **kwargs): def main(): + if os.environ['GOMA_DIR']: + RunCmd([VPYTHON, os.path.join(os.environ['GOMA_DIR'], 'goma_ctl.py'), 'start']) + RunCmd([VPYTHON, 'flutter/tools/gn'], cwd=SRC_DIR) + RunCmd([AUTONINJA, '-C', 'out/host_debug', 'font-subset'], cwd=SRC_DIR) failures = 0 for should_pass, golden_font, input_font, codepoints in COMPARE_TESTS: gen_ttf = os.path.join(SCRIPT_DIR, 'gen', golden_font) From d0ad0f9380b4ba334d404a0c2f1c26d908c781ae Mon Sep 17 00:00:00 2001 From: Dan Field Date: Sun, 29 Dec 2019 10:31:18 -0800 Subject: [PATCH 12/23] .. --- testing/run_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/run_tests.py b/testing/run_tests.py index 2119cf8bc8a18..93092dea78963 100755 --- a/testing/run_tests.py +++ b/testing/run_tests.py @@ -22,7 +22,7 @@ dart_tests_dir = os.path.join(buildroot_dir, 'flutter', 'testing', 'dart',) font_subset_dir = os.path.join(buildroot_dir, 'flutter', 'tools', 'font-subset') -BAT = '.bat' if IsWindows() else '' +BAT = '.bat' if sys.platform.startswith(('cygwin', 'win')) else '' VPYTHON = 'vpython' + BAT fml_unittests_filter = '--gtest_filter=-*TimeSensitiveTest*:*GpuThreadMerger*' From 3eb8023945b22ab1bf7882c59d6113ce82dfc8aa Mon Sep 17 00:00:00 2001 From: Dan Field Date: Sun, 29 Dec 2019 10:36:56 -0800 Subject: [PATCH 13/23] bugs in test script --- tools/font-subset/fixtures/3.ttf | Bin 1252 -> 1356 bytes tools/font-subset/test.py | 11 ++++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/tools/font-subset/fixtures/3.ttf b/tools/font-subset/fixtures/3.ttf index 4bb2c2c44d9ae32913cb8c5ee97b7acfd1e77ee8..35181456b2e8a8b1854d446b6f63518bf7f7aec3 100644 GIT binary patch delta 428 zcmXYsy-UMT6vgjNqBTXUBBe-$HdLw5qS8_w4N{Q0I4C$N3F#Nu*g!w1f|x;_l`amB z{R4!8i{R|g!KFBe5XC`UodmI-v_81+zTdg$zW46C-0_|e6VWKyB$B$hwV3!!|C~YT zAT-0&Ehnq55pf(`GV0YZO>!E11(phW#p?CYNqh|+DU=TLK4tqE{DG(!b9z=Delc*q zL4COhL1IB55;gE_(X3_~!an68oI>!H%7)HCx<^CHWm9ihv`4R~Ux330x|s`prH;Y4 zkdLf#rP@2E1kq6Z@?o8o<^)G~)A*4)m7BfDGnmg^^ZlI&?>qRjBZ1*_ltI8P^*zJL zOoAgh3Z{eC&7t4Wo9k}cJUxCl=p+`O9#_XQ?H2FwhD0%nahJPjm&r#{xIO8QL_I43 ue?$y;BB?n~G?iqnR#P=qwZp1fV?Pe8+O9AFS8aX^OLFZ04XM2#-tr#|VOc%^ delta 363 zcmX@Z^@OvYfsuiMfrp`iftkUZ; zU=U<<0rFdbe3taY;)4JGfjSu&JAizS^qk7H_mzHiK>h_F*2+jtOyOj^n+#;n0Lq(X z00meWnV1+DnEn9yDjB&Yo)t_E3~3Au%nCpOj-34DM1};0eLxKpK>XaqiUNjMhKE3T z4Ip13FEKY&`m<>*knaIhQ&o^(T=Kt-!3gMadGj>pZHxe(bWFkk diff --git a/tools/font-subset/test.py b/tools/font-subset/test.py index 24d1428a93059..ecf55b7d260f9 100755 --- a/tools/font-subset/test.py +++ b/tools/font-subset/test.py @@ -27,9 +27,10 @@ (True, '1.ttf', MATERIAL_TTF, [r'57347']), (True, '1.ttf', MATERIAL_TTF, [r'0xE003']), (True, '1.ttf', MATERIAL_TTF, [r'\uE003']), - (False, '1.ttf', MATERIAL_TTF, [r'57348']), + (False, '1.ttf', MATERIAL_TTF, [r'57348']), # False because different codepoint (True, '2.ttf', MATERIAL_TTF, [r'0xE003', r'0xE004']), - (True, '3.ttf', MATERIAL_TTF, [r'0xE003', r'0xE004', r'57347',]), + (True, '2.ttf', MATERIAL_TTF, [r'0xE003', r'0xE004', r'57347',]), # Duplicated codepoint + (True, '3.ttf', MATERIAL_TTF, [r'0xE003', r'0xE004', r'0xE021',]), ) FAIL_TESTS = [ @@ -49,7 +50,7 @@ def RunCmd(cmd, **kwargs): def main(): - if os.environ['GOMA_DIR']: + if 'GOMA_DIR' in os.environ: RunCmd([VPYTHON, os.path.join(os.environ['GOMA_DIR'], 'goma_ctl.py'), 'start']) RunCmd([VPYTHON, 'flutter/tools/gn'], cwd=SRC_DIR) RunCmd([AUTONINJA, '-C', 'out/host_debug', 'font-subset'], cwd=SRC_DIR) @@ -73,8 +74,8 @@ def main(): failures += 1 devnull.close() - if failures > 1: - print('%s tests failed.' % failures) + if failures > 0: + print('%s test(s) failed.' % failures) return 1 print('All tests passed') From 90ffe4ebf2393991f453d81476b966d20e17d81a Mon Sep 17 00:00:00 2001 From: Dan Field Date: Sun, 29 Dec 2019 14:38:13 -0800 Subject: [PATCH 14/23] .. --- testing/run_tests.py | 5 +---- tools/font-subset/test.py | 5 ++--- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/testing/run_tests.py b/testing/run_tests.py index 93092dea78963..0827f387d2822 100755 --- a/testing/run_tests.py +++ b/testing/run_tests.py @@ -22,9 +22,6 @@ dart_tests_dir = os.path.join(buildroot_dir, 'flutter', 'testing', 'dart',) font_subset_dir = os.path.join(buildroot_dir, 'flutter', 'tools', 'font-subset') -BAT = '.bat' if sys.platform.startswith(('cygwin', 'win')) else '' -VPYTHON = 'vpython' + BAT - fml_unittests_filter = '--gtest_filter=-*TimeSensitiveTest*:*GpuThreadMerger*' def RunCmd(cmd, **kwargs): @@ -360,7 +357,7 @@ def main(): RunEngineBenchmarks(build_dir, engine_filter) if 'engine' in types or 'font-subset' in types: - RunCmd([VPYTHON, 'test.py'], cwd=font_subset_dir) + RunCmd(['python', 'test.py'], cwd=font_subset_dir) if __name__ == '__main__': diff --git a/tools/font-subset/test.py b/tools/font-subset/test.py index ecf55b7d260f9..75bf2fc4f1cd7 100755 --- a/tools/font-subset/test.py +++ b/tools/font-subset/test.py @@ -19,7 +19,6 @@ IS_WINDOWS = sys.platform.startswith(('cygwin', 'win')) EXE = '.exe' if IS_WINDOWS else '' BAT = '.bat' if IS_WINDOWS else '' -VPYTHON = 'vpython' + BAT AUTONINJA = 'autoninja' + BAT FONT_SUBSET = os.path.join(SRC_DIR, 'out', 'host_debug', 'font-subset' + EXE) @@ -51,8 +50,8 @@ def RunCmd(cmd, **kwargs): def main(): if 'GOMA_DIR' in os.environ: - RunCmd([VPYTHON, os.path.join(os.environ['GOMA_DIR'], 'goma_ctl.py'), 'start']) - RunCmd([VPYTHON, 'flutter/tools/gn'], cwd=SRC_DIR) + RunCmd(['python', os.path.join(os.environ['GOMA_DIR'], 'goma_ctl.py'), 'start']) + RunCmd(['python', 'flutter/tools/gn'], cwd=SRC_DIR) RunCmd([AUTONINJA, '-C', 'out/host_debug', 'font-subset'], cwd=SRC_DIR) failures = 0 for should_pass, golden_font, input_font, codepoints in COMPARE_TESTS: From 6f29513ac72119be3aea50ce49b32f38a8818dc5 Mon Sep 17 00:00:00 2001 From: Dan Field Date: Thu, 2 Jan 2020 11:48:23 -0800 Subject: [PATCH 15/23] ensure gen folder --- tools/font-subset/gen/.gitkeep | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tools/font-subset/gen/.gitkeep diff --git a/tools/font-subset/gen/.gitkeep b/tools/font-subset/gen/.gitkeep new file mode 100644 index 0000000000000..e69de29bb2d1d From c04a8c15ab48ac9fd106423902615104c5401ee3 Mon Sep 17 00:00:00 2001 From: Dan Field Date: Thu, 2 Jan 2020 12:32:09 -0800 Subject: [PATCH 16/23] Make it more flexible, require build beforehand --- tools/font-subset/test.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/tools/font-subset/test.py b/tools/font-subset/test.py index 75bf2fc4f1cd7..3ede85d35b9d1 100755 --- a/tools/font-subset/test.py +++ b/tools/font-subset/test.py @@ -19,8 +19,11 @@ IS_WINDOWS = sys.platform.startswith(('cygwin', 'win')) EXE = '.exe' if IS_WINDOWS else '' BAT = '.bat' if IS_WINDOWS else '' -AUTONINJA = 'autoninja' + BAT FONT_SUBSET = os.path.join(SRC_DIR, 'out', 'host_debug', 'font-subset' + EXE) +if not os.path.isfile(FONT_SUBSET): + FONT_SUBSET = os.path.join(SRC_DIR, 'out', 'host_debug_unopt', 'font-subset' + EXE) +if not os.path.isfile(FONT_SUBSET): + raise Exception('Could not locate font-subset%s in host_debug or host_debug_unopt - build before running this script.' % EXE) COMPARE_TESTS = ( (True, '1.ttf', MATERIAL_TTF, [r'57347']), @@ -49,10 +52,7 @@ def RunCmd(cmd, **kwargs): def main(): - if 'GOMA_DIR' in os.environ: - RunCmd(['python', os.path.join(os.environ['GOMA_DIR'], 'goma_ctl.py'), 'start']) - RunCmd(['python', 'flutter/tools/gn'], cwd=SRC_DIR) - RunCmd([AUTONINJA, '-C', 'out/host_debug', 'font-subset'], cwd=SRC_DIR) + print('Using font subset binary at %s' % FONT_SUBSET) failures = 0 for should_pass, golden_font, input_font, codepoints in COMPARE_TESTS: gen_ttf = os.path.join(SCRIPT_DIR, 'gen', golden_font) @@ -65,13 +65,12 @@ def main(): print('Test case %s failed.' % cmd) failures += 1 - devnull = open(os.devnull, 'w') - for cmd in FAIL_TESTS: - print('Running test %s...' % cmd) - if subprocess.call(cmd, cwd=SRC_DIR, stdout=devnull, stderr=devnull) == 0: - print('Command %s passed, expected failure.' % cmd) - failures += 1 - devnull.close() + with open(os.devnull, 'w') as devnull: + for cmd in FAIL_TESTS: + print('Running test %s...' % cmd) + if subprocess.call(cmd, cwd=SRC_DIR, stdout=devnull, stderr=devnull) == 0: + print('Command %s passed, expected failure.' % cmd) + failures += 1 if failures > 0: print('%s test(s) failed.' % failures) From 1eff22ce2f97933b6c454b42e6b7b430d9c62ca4 Mon Sep 17 00:00:00 2001 From: Dan Field Date: Thu, 2 Jan 2020 14:56:32 -0800 Subject: [PATCH 17/23] fail if cannot create file --- tools/font-subset/main.cc | 7 +++++++ tools/font-subset/test.py | 3 ++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/tools/font-subset/main.cc b/tools/font-subset/main.cc index f801bdcc5dc6b..18efb16998d64 100644 --- a/tools/font-subset/main.cc +++ b/tools/font-subset/main.cc @@ -110,6 +110,13 @@ int main(int argc, char** argv) { std::ofstream output_font_file; output_font_file.open(output_file_path, std::ios::out | std::ios::trunc | std::ios::binary); + if (!output_font_file.is_open()) { + std::cerr << "Failed to open output file '" << output_file_path + << "'. The parent directory may not exist, or the user does not " + "have permission to create this file." + << std::endl; + return -1; + } output_font_file.write(data, data_length); output_font_file.flush(); output_font_file.close(); diff --git a/tools/font-subset/test.py b/tools/font-subset/test.py index 3ede85d35b9d1..66fba6d32cd8e 100755 --- a/tools/font-subset/test.py +++ b/tools/font-subset/test.py @@ -40,7 +40,8 @@ [FONT_SUBSET, 'output.ttf', MATERIAL_TTF, '0xFFFFFFFF'], # Value too big. [FONT_SUBSET, 'output.ttf', MATERIAL_TTF, '-1'], # invalid value [FONT_SUBSET, 'output.ttf', MATERIAL_TTF, 'foo'], # no valid values - [FONT_SUBSET, 'output.ttf', MATERIAL_TTF, r'0xE003', r'0x12', r'0xE004',] # codepoint not in font + [FONT_SUBSET, 'output.ttf', MATERIAL_TTF, r'0xE003', r'0x12', r'0xE004',], # codepoint not in font + [FONT_SUBSET, 'non-existant-dir/output.ttf', MATERIAL_TTF, r'0xE003',], # dir doesn't exist ] def RunCmd(cmd, **kwargs): From 492987a41662a81d71d07c79dacfe92808d9795b Mon Sep 17 00:00:00 2001 From: Dan Field Date: Mon, 6 Jan 2020 11:04:54 -0800 Subject: [PATCH 18/23] .. --- tools/font-subset/main.cc | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/tools/font-subset/main.cc b/tools/font-subset/main.cc index 18efb16998d64..915d77c7a8577 100644 --- a/tools/font-subset/main.cc +++ b/tools/font-subset/main.cc @@ -81,14 +81,15 @@ int main(int argc, char** argv) { hb_face_collect_unicodes(font_face.get(), actual_codepoints.get()); for (int i = 3; i < argc; i++) { auto codepoint = ParseCodepoint(argv[i]); - if (codepoint) { - if (!hb_set_has(actual_codepoints.get(), codepoint)) { - std::cerr << "Codepoint " << argv[i] - << " not found in font, aborting." << std::endl; - return -1; - } - hb_set_add(desired_codepoints, codepoint); + if (!codepoint) { + continue; } + if (!hb_set_has(actual_codepoints.get(), codepoint)) { + std::cerr << "Codepoint " << argv[i] + << " not found in font, aborting." << std::endl; + return -1; + } + hb_set_add(desired_codepoints, codepoint); } } HarfbuzzWrappers::HbFacePtr new_face(hb_subset(font_face.get(), input.get())); From c9f999945eb1a939d95ca4174f878cd7d453afaf Mon Sep 17 00:00:00 2001 From: Dan Field Date: Mon, 6 Jan 2020 13:55:59 -0800 Subject: [PATCH 19/23] stdin --- tools/font-subset/main.cc | 27 +++++++++--------- tools/font-subset/test.py | 59 ++++++++++++++++++++++++++------------- 2 files changed, 53 insertions(+), 33 deletions(-) diff --git a/tools/font-subset/main.cc b/tools/font-subset/main.cc index 915d77c7a8577..96c0acec491b3 100644 --- a/tools/font-subset/main.cc +++ b/tools/font-subset/main.cc @@ -12,15 +12,15 @@ #include "hb_wrappers.h" -hb_codepoint_t ParseCodepoint(const char* arg) { +hb_codepoint_t ParseCodepoint(const std::string& arg) { unsigned long value = 0; // Check for \u123, u123, otherwise let strtoul work it out. if (arg[0] == 'u') { - value = strtoul(arg + 1, nullptr, 16); + value = strtoul(arg.c_str() + 1, nullptr, 16); } else if (arg[0] == '\\' && arg[1] == 'u') { - value = strtoul(arg + 2, nullptr, 16); + value = strtoul(arg.c_str() + 2, nullptr, 16); } else { - value = strtoul(arg, nullptr, 0); + value = strtoul(arg.c_str(), nullptr, 0); } if (value == 0 || value > std::numeric_limits::max()) { std::cerr << "The value '" << arg << "' (" << value @@ -33,16 +33,16 @@ hb_codepoint_t ParseCodepoint(const char* arg) { void Usage() { std::cout << "Usage:" << std::endl; - std::cout << "font-subset [CODEPOINTS]" << std::endl; + std::cout << "font-subset " << std::endl; std::cout << std::endl; std::cout << "The output.ttf file will be overwritten if it exists already " "and the subsetting operation succeeds." << std::endl; - std::cout << "At least one code point must be specified. The code points " - "should be separated by spaces, and must be input as decimal " - "numbers (123), hexidecimal numbers (0x7B), or unicode " - "hexidecimal characters (\\u7B)." + std::cout << "Codepoints should be specified on stdin, separated by spaces, " + "and must be input as decimal numbers (123), hexidecimal " + "numbers (0x7B), or unicode hexidecimal characters (\\u7B)." << std::endl; + std::cout << "Input terminates with a newline." << std::endl; std::cout << "This program will de-duplicate codepoints if the same codepoint is " "specified multiple times, e.g. '123 123' will be treated as '123'." @@ -50,7 +50,7 @@ void Usage() { } int main(int argc, char** argv) { - if (argc <= 3) { + if (argc != 3) { Usage(); return -1; } @@ -79,13 +79,14 @@ int main(int argc, char** argv) { hb_set_t* desired_codepoints = hb_subset_input_unicode_set(input.get()); HarfbuzzWrappers::HbSetPtr actual_codepoints(hb_set_create()); hb_face_collect_unicodes(font_face.get(), actual_codepoints.get()); - for (int i = 3; i < argc; i++) { - auto codepoint = ParseCodepoint(argv[i]); + std::string raw_codepoint; + while (std::cin >> raw_codepoint) { + auto codepoint = ParseCodepoint(raw_codepoint); if (!codepoint) { continue; } if (!hb_set_has(actual_codepoints.get(), codepoint)) { - std::cerr << "Codepoint " << argv[i] + std::cerr << "Codepoint " << raw_codepoint << " not found in font, aborting." << std::endl; return -1; } diff --git a/tools/font-subset/test.py b/tools/font-subset/test.py index 66fba6d32cd8e..f5974e9e6a18a 100755 --- a/tools/font-subset/test.py +++ b/tools/font-subset/test.py @@ -14,7 +14,7 @@ import sys SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) -SRC_DIR = os.path.join(SCRIPT_DIR, '..', '..', '..') +SRC_DIR = os.path.normpath(os.path.join(SCRIPT_DIR, '..', '..', '..')) MATERIAL_TTF = os.path.join(SCRIPT_DIR, 'fixtures', 'MaterialIcons-Regular.ttf') IS_WINDOWS = sys.platform.startswith(('cygwin', 'win')) EXE = '.exe' if IS_WINDOWS else '' @@ -36,20 +36,42 @@ ) FAIL_TESTS = [ - [FONT_SUBSET, 'output.ttf', 'does-not-exist.ttf', '1'], # non-existant input font - [FONT_SUBSET, 'output.ttf', MATERIAL_TTF, '0xFFFFFFFF'], # Value too big. - [FONT_SUBSET, 'output.ttf', MATERIAL_TTF, '-1'], # invalid value - [FONT_SUBSET, 'output.ttf', MATERIAL_TTF, 'foo'], # no valid values - [FONT_SUBSET, 'output.ttf', MATERIAL_TTF, r'0xE003', r'0x12', r'0xE004',], # codepoint not in font - [FONT_SUBSET, 'non-existant-dir/output.ttf', MATERIAL_TTF, r'0xE003',], # dir doesn't exist + ([FONT_SUBSET, 'output.ttf', 'does-not-exist.ttf'], ['1',]), # non-existant input font + ([FONT_SUBSET, 'output.ttf', MATERIAL_TTF], ['0xFFFFFFFF',]), # Value too big. + ([FONT_SUBSET, 'output.ttf', MATERIAL_TTF], ['-1',]), # invalid value + ([FONT_SUBSET, 'output.ttf', MATERIAL_TTF], ['foo',]), # no valid values + ([FONT_SUBSET, 'output.ttf', MATERIAL_TTF], ['0xE003', '0x12', '0xE004',]), # codepoint not in font + ([FONT_SUBSET, 'non-existant-dir/output.ttf', MATERIAL_TTF], ['0xE003',]), # dir doesn't exist ] -def RunCmd(cmd, **kwargs): - try: - print(subprocess.check_output(cmd, **kwargs)) - except subprocess.CalledProcessError as cpe: - print(cpe.output) - raise cpe +def RunCmd(cmd, codepoints, fail=False): + print('Running command:') + print(' %s' % ' '.join(cmd)) + print('STDIN: %s' % ' '.join(codepoints)) + p = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stdin=subprocess.PIPE, + stderr=subprocess.PIPE, + cwd=SRC_DIR + ) + stdout_data, stderr_data = p.communicate(input=' '.join(codepoints)) + if p.returncode != 0 and fail == False: + print('FAILURE: %s' % p.returncode) + print('STDOUT:') + print(stdout_data) + print('STDERR:') + print(stderr_data) + elif p.returncode == 0 and fail == True: + print('FAILURE - test passed but should have failed.') + print('STDOUT:') + print(stdout_data) + print('STDERR:') + print(stderr_data) + else: + print('Success.') + + return p.returncode def main(): @@ -58,19 +80,16 @@ def main(): for should_pass, golden_font, input_font, codepoints in COMPARE_TESTS: gen_ttf = os.path.join(SCRIPT_DIR, 'gen', golden_font) golden_ttf = os.path.join(SCRIPT_DIR, 'fixtures', golden_font) - cmd = [FONT_SUBSET, gen_ttf, input_font] + codepoints - print('Running test %s...' % cmd) - RunCmd(cmd, cwd=SRC_DIR) + cmd = [FONT_SUBSET, gen_ttf, input_font] + RunCmd(cmd, codepoints) cmp = filecmp.cmp(gen_ttf, golden_ttf, shallow=False) if (should_pass and not cmp) or (not should_pass and cmp): print('Test case %s failed.' % cmd) failures += 1 with open(os.devnull, 'w') as devnull: - for cmd in FAIL_TESTS: - print('Running test %s...' % cmd) - if subprocess.call(cmd, cwd=SRC_DIR, stdout=devnull, stderr=devnull) == 0: - print('Command %s passed, expected failure.' % cmd) + for cmd, codepoints in FAIL_TESTS: + if RunCmd(cmd, codepoints, fail=True) == 0: failures += 1 if failures > 0: From 3ef73cbd4610a0402f01338c479aa56de81f726c Mon Sep 17 00:00:00 2001 From: Dan Field Date: Mon, 6 Jan 2020 14:21:59 -0800 Subject: [PATCH 20/23] one more failure mode --- tools/font-subset/main.cc | 8 +++++++- tools/font-subset/test.py | 3 ++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/tools/font-subset/main.cc b/tools/font-subset/main.cc index 96c0acec491b3..552a7065d3318 100644 --- a/tools/font-subset/main.cc +++ b/tools/font-subset/main.cc @@ -83,7 +83,8 @@ int main(int argc, char** argv) { while (std::cin >> raw_codepoint) { auto codepoint = ParseCodepoint(raw_codepoint); if (!codepoint) { - continue; + std::cerr << "Invalid codepoint for " << raw_codepoint << "; exiting." << std::endl; + return -1; } if (!hb_set_has(actual_codepoints.get(), codepoint)) { std::cerr << "Codepoint " << raw_codepoint @@ -92,7 +93,12 @@ int main(int argc, char** argv) { } hb_set_add(desired_codepoints, codepoint); } + if (hb_set_is_empty(desired_codepoints)) { + std::cerr << "No codepoints specified, exiting." << std::endl; + return -1; + } } + HarfbuzzWrappers::HbFacePtr new_face(hb_subset(font_face.get(), input.get())); if (new_face.get() == hb_face_get_empty()) { diff --git a/tools/font-subset/test.py b/tools/font-subset/test.py index f5974e9e6a18a..8219c184c62a1 100755 --- a/tools/font-subset/test.py +++ b/tools/font-subset/test.py @@ -42,12 +42,13 @@ ([FONT_SUBSET, 'output.ttf', MATERIAL_TTF], ['foo',]), # no valid values ([FONT_SUBSET, 'output.ttf', MATERIAL_TTF], ['0xE003', '0x12', '0xE004',]), # codepoint not in font ([FONT_SUBSET, 'non-existant-dir/output.ttf', MATERIAL_TTF], ['0xE003',]), # dir doesn't exist + ([FONT_SUBSET, 'output.ttf', MATERIAL_TTF], [' ',]), # empty input ] def RunCmd(cmd, codepoints, fail=False): print('Running command:') print(' %s' % ' '.join(cmd)) - print('STDIN: %s' % ' '.join(codepoints)) + print('STDIN: "%s"' % ' '.join(codepoints)) p = subprocess.Popen( cmd, stdout=subprocess.PIPE, From 99ec982a05c0745490426e0521fc1d13ab825542 Mon Sep 17 00:00:00 2001 From: Dan Field Date: Mon, 6 Jan 2020 14:33:29 -0800 Subject: [PATCH 21/23] format --- tools/font-subset/main.cc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tools/font-subset/main.cc b/tools/font-subset/main.cc index 552a7065d3318..a0d9c1a71e549 100644 --- a/tools/font-subset/main.cc +++ b/tools/font-subset/main.cc @@ -83,7 +83,8 @@ int main(int argc, char** argv) { while (std::cin >> raw_codepoint) { auto codepoint = ParseCodepoint(raw_codepoint); if (!codepoint) { - std::cerr << "Invalid codepoint for " << raw_codepoint << "; exiting." << std::endl; + std::cerr << "Invalid codepoint for " << raw_codepoint << "; exiting." + << std::endl; return -1; } if (!hb_set_has(actual_codepoints.get(), codepoint)) { From 3fdc8ceeb79e79cc094b37c80faf20c69eccde04 Mon Sep 17 00:00:00 2001 From: Dan Field Date: Mon, 6 Jan 2020 14:34:51 -0800 Subject: [PATCH 22/23] more tests! --- tools/font-subset/test.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tools/font-subset/test.py b/tools/font-subset/test.py index 8219c184c62a1..b25422ba2f850 100755 --- a/tools/font-subset/test.py +++ b/tools/font-subset/test.py @@ -43,6 +43,8 @@ ([FONT_SUBSET, 'output.ttf', MATERIAL_TTF], ['0xE003', '0x12', '0xE004',]), # codepoint not in font ([FONT_SUBSET, 'non-existant-dir/output.ttf', MATERIAL_TTF], ['0xE003',]), # dir doesn't exist ([FONT_SUBSET, 'output.ttf', MATERIAL_TTF], [' ',]), # empty input + ([FONT_SUBSET, 'output.ttf', MATERIAL_TTF], []), # empty input + ([FONT_SUBSET, 'output.ttf', MATERIAL_TTF], ['']), # empty input ] def RunCmd(cmd, codepoints, fail=False): From 173885b31cc2847c63bf84acd7edd5fbe41add4f Mon Sep 17 00:00:00 2001 From: Dan Field Date: Mon, 6 Jan 2020 19:47:45 -0800 Subject: [PATCH 23/23] ci