mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
259 lines
8.4 KiB
Dart
259 lines
8.4 KiB
Dart
// 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.
|
|
// @dart = 2.6
|
|
import 'dart:io' as io;
|
|
import 'package:image/image.dart';
|
|
import 'package:meta/meta.dart';
|
|
import 'package:path/path.dart' as path;
|
|
import 'package:yaml/yaml.dart';
|
|
|
|
import 'environment.dart';
|
|
import 'utils.dart';
|
|
|
|
/// How to compares pixels within the image.
|
|
///
|
|
/// Keep this enum in sync with the one defined in `golden_tester.dart`.
|
|
enum PixelComparison {
|
|
/// Allows minor blur and anti-aliasing differences by comparing a 3x3 grid
|
|
/// surrounding the pixel rather than direct 1:1 comparison.
|
|
fuzzy,
|
|
|
|
/// Compares one pixel at a time.
|
|
///
|
|
/// Anti-aliasing or blur will result in higher diff rate.
|
|
precise,
|
|
}
|
|
|
|
void main(List<String> args) {
|
|
final io.File fileA = io.File(args[0]);
|
|
final io.File fileB = io.File(args[1]);
|
|
final Image imageA = decodeNamedImage(fileA.readAsBytesSync(), 'a.png');
|
|
final Image imageB = decodeNamedImage(fileB.readAsBytesSync(), 'b.png');
|
|
final ImageDiff diff = ImageDiff(golden: imageA, other: imageB, pixelComparison: PixelComparison.fuzzy);
|
|
print('Diff: ${(diff.rate * 100).toStringAsFixed(4)}%');
|
|
}
|
|
|
|
/// This class encapsulates visually diffing an Image with any other.
|
|
/// Both images need to be the exact same size.
|
|
class ImageDiff {
|
|
|
|
/// The image to match
|
|
final Image golden;
|
|
|
|
/// The image being compared
|
|
final Image other;
|
|
|
|
/// Algorithm used for comparing pixels.
|
|
final PixelComparison pixelComparison;
|
|
|
|
/// The output of the comparison
|
|
/// Pixels in the output image can have 3 different colors depending on the comparison
|
|
/// between golden pixels and other pixels:
|
|
/// * white: when both pixels are the same
|
|
/// * red: when a pixel is found in other, but not in golden
|
|
/// * green: when a pixel is found in golden, but not in other
|
|
Image diff;
|
|
|
|
/// The ratio of wrong pixels to all pixels in golden (between 0 and 1)
|
|
/// This gets set to 1 (100% difference) when golden and other aren't the same size.
|
|
double get rate => _wrongPixels / _pixelCount;
|
|
|
|
ImageDiff({
|
|
@required this.golden,
|
|
@required this.other,
|
|
@required this.pixelComparison,
|
|
}) {
|
|
_computeDiff();
|
|
}
|
|
|
|
int _pixelCount = 0;
|
|
int _wrongPixels = 0;
|
|
|
|
/// That would be the distance between black and white.
|
|
static final double _maxTheoreticalColorDistance = Color.distance(
|
|
<num>[255, 255, 255], // white
|
|
<num>[0, 0, 0], // black
|
|
false,
|
|
).toDouble();
|
|
|
|
// If the normalized color difference of a pixel is greater than this number,
|
|
// we consider it a wrong pixel.
|
|
static const double _kColorDistanceThreshold = 0.1;
|
|
|
|
final int _colorOk = Color.fromRgb(255, 255, 255);
|
|
final int _colorBadPixel = Color.fromRgb(255, 0, 0);
|
|
final int _colorExpectedPixel = Color.fromRgb(0, 255, 0);
|
|
|
|
/// Reads a pixel value out of [image] at [x] and [y].
|
|
///
|
|
/// If the pixel is out of bounds, reflects the [x] and [y] coordinates off
|
|
/// the border back into the image treating the border like a mirror.
|
|
static int _reflectedPixel(Image image, int x, int y) {
|
|
x = x.abs();
|
|
if (x == image.width) {
|
|
x = image.width - 2;
|
|
}
|
|
|
|
y = y.abs();
|
|
if (y == image.height) {
|
|
y = image.height - 2;
|
|
}
|
|
|
|
return image.getPixel(x, y);
|
|
}
|
|
|
|
static int _average(Iterable<int> values) {
|
|
return values.reduce((a, b) => a + b) ~/ values.length;
|
|
}
|
|
|
|
/// The value of the pixel at [x] and [y] coordinates.
|
|
///
|
|
/// If [pixelComparison] is [PixelComparison.precise], reads the RGB value of
|
|
/// the pixel.
|
|
///
|
|
/// If [pixelComparison] is [PixelComparison.fuzzy], reads the RGB values of
|
|
/// the average of the 3x3 box of pixels centered at [x] and [y].
|
|
List<int> _getPixelRgbForComparison(Image image, int x, int y) {
|
|
switch (pixelComparison) {
|
|
case PixelComparison.fuzzy:
|
|
final List<int> pixels = <int>[
|
|
_reflectedPixel(image, x - 1, y - 1),
|
|
_reflectedPixel(image, x - 1, y),
|
|
_reflectedPixel(image, x - 1, y + 1),
|
|
|
|
_reflectedPixel(image, x, y - 1),
|
|
_reflectedPixel(image, x, y),
|
|
_reflectedPixel(image, x, y + 1),
|
|
|
|
_reflectedPixel(image, x + 1, y - 1),
|
|
_reflectedPixel(image, x + 1, y),
|
|
_reflectedPixel(image, x + 1, y + 1),
|
|
];
|
|
return <int>[
|
|
_average(pixels.map((p) => getRed(p))),
|
|
_average(pixels.map((p) => getGreen(p))),
|
|
_average(pixels.map((p) => getBlue(p))),
|
|
];
|
|
case PixelComparison.precise:
|
|
final int pixel = image.getPixel(x, y);
|
|
return <int>[
|
|
getRed(pixel),
|
|
getGreen(pixel),
|
|
getBlue(pixel),
|
|
];
|
|
default:
|
|
throw 'Unrecognized pixel comparison value: ${pixelComparison}';
|
|
}
|
|
}
|
|
|
|
void _computeDiff() {
|
|
int goldenWidth = golden.width;
|
|
int goldenHeight = golden.height;
|
|
|
|
_pixelCount = goldenWidth * goldenHeight;
|
|
diff = Image(goldenWidth, goldenHeight);
|
|
|
|
if (goldenWidth == other.width && goldenHeight == other.height) {
|
|
for(int y = 0; y < goldenHeight; y++) {
|
|
for (int x = 0; x < goldenWidth; x++) {
|
|
final bool isExactlySame = golden.getPixel(x, y) == other.getPixel(x, y);
|
|
final List<int> goldenPixel = _getPixelRgbForComparison(golden, x, y);
|
|
final List<int> otherPixel = _getPixelRgbForComparison(other, x, y);
|
|
final double colorDistance = Color.distance(goldenPixel, otherPixel, false) / _maxTheoreticalColorDistance;
|
|
final bool isFuzzySame = colorDistance < _kColorDistanceThreshold;
|
|
if (isExactlySame || isFuzzySame) {
|
|
diff.setPixel(x, y, _colorOk);
|
|
} else {
|
|
final int goldenLuminance = getLuminanceRgb(goldenPixel[0], goldenPixel[1], goldenPixel[2]);
|
|
final int otherLuminance = getLuminanceRgb(otherPixel[0], otherPixel[1], otherPixel[2]);
|
|
if (goldenLuminance < otherLuminance) {
|
|
diff.setPixel(x, y, _colorExpectedPixel);
|
|
} else {
|
|
diff.setPixel(x, y, _colorBadPixel);
|
|
}
|
|
_wrongPixels++;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// Images are completely different resolutions. Bail out big time.
|
|
_wrongPixels = _pixelCount;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Returns text explaining pixel difference rate.
|
|
String getPrintableDiffFilesInfo(double diffRate, double maxRate) =>
|
|
'(${((diffRate) * 100).toStringAsFixed(4)}% of pixels were different. '
|
|
'Maximum allowed rate is: ${(maxRate * 100).toStringAsFixed(4)}%).';
|
|
|
|
/// Fetches golden files from github.com/flutter/goldens, cloning the repository if necessary.
|
|
///
|
|
/// The repository is cloned into web_ui/.dart_tool.
|
|
Future<void> fetchGoldens() async {
|
|
await _GoldensRepoFetcher().fetch();
|
|
}
|
|
|
|
class _GoldensRepoFetcher {
|
|
String _repository;
|
|
String _revision;
|
|
|
|
Future<void> fetch() async {
|
|
final io.File lockFile = io.File(
|
|
path.join(environment.webUiDevDir.path, 'goldens_lock.yaml')
|
|
);
|
|
final YamlMap lock = loadYaml(lockFile.readAsStringSync()) as YamlMap;
|
|
_repository = lock['repository'] as String;
|
|
_revision = lock['revision'] as String;
|
|
|
|
final String localRevision = await _getLocalRevision();
|
|
if (localRevision == _revision) {
|
|
return;
|
|
}
|
|
|
|
print('Fetching $_repository@$_revision');
|
|
|
|
if (!environment.webUiGoldensRepositoryDirectory.existsSync()) {
|
|
environment.webUiGoldensRepositoryDirectory.createSync(recursive: true);
|
|
await runProcess(
|
|
'git',
|
|
<String>['init'],
|
|
workingDirectory: environment.webUiGoldensRepositoryDirectory.path,
|
|
mustSucceed: true,
|
|
);
|
|
await runProcess(
|
|
'git',
|
|
<String>['remote', 'add', 'origin', _repository],
|
|
workingDirectory: environment.webUiGoldensRepositoryDirectory.path,
|
|
mustSucceed: true,
|
|
);
|
|
}
|
|
|
|
await runProcess(
|
|
'git',
|
|
<String>['fetch', 'origin', 'master'],
|
|
workingDirectory: environment.webUiGoldensRepositoryDirectory.path,
|
|
mustSucceed: true,
|
|
);
|
|
await runProcess(
|
|
'git',
|
|
<String>['checkout', _revision],
|
|
workingDirectory: environment.webUiGoldensRepositoryDirectory.path,
|
|
mustSucceed: true,
|
|
);
|
|
}
|
|
|
|
Future<String> _getLocalRevision() async {
|
|
final io.File head = io.File(path.join(
|
|
environment.webUiGoldensRepositoryDirectory.path, '.git', 'HEAD'
|
|
));
|
|
|
|
if (!head.existsSync()) {
|
|
return null;
|
|
}
|
|
|
|
return head.readAsStringSync().trim();
|
|
}
|
|
}
|