XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFESystem

【Rust】XFE系统,基于Rust和汇编

公开
关注 0 Fork 0 Star 0
UTF-8
"""Compare unmodified QEMU framebuffer screenshots with website browser references.

This postprocessor does not create a native screenshot, run a browser, replace
fonts, resize away differences or treat a host image as guest acceptance.
"""
import argparse
import hashlib
import json
import math
from pathlib import Path

from PIL import Image, ImageChops, ImageDraw, ImageStat


def digest(path):
    return hashlib.sha256(Path(path).read_bytes()).hexdigest()


def read(path):
    return json.loads(Path(path).read_text('utf-8-sig'))


def find_screenshots(value):
    if isinstance(value, dict):
        if isinstance(value.get('Path'), str) and isinstance(value.get('Sha256'), str):
            yield value
        for child in value.values():
            yield from find_screenshots(child)
    elif isinstance(value, list):
        for child in value:
            yield from find_screenshots(child)


def verify_native(path, receipt_path, allow_failed=False):
    receipt = read(receipt_path)
    assert receipt.get('Kind') == 'BareMetalNativeAotBootProof', 'Reference is not a native QEMU boot receipt'
    assert receipt.get('Runtime', '').startswith('Cosmos NativeAOT'), 'Native runtime provenance missing'
    assert receipt.get('QemuArgs') and '-qmp' in receipt['QemuArgs'], 'QEMU invocation is absent'
    assert receipt.get('KernelSha256') and receipt.get('SourceSnapshotSha256') and receipt.get('ImageSha256'), 'Compiled image/source binding is absent'
    snapshot = Path(receipt['SourceSnapshotPath'])
    assert snapshot.exists() and digest(snapshot) == receipt['SourceSnapshotSha256'], 'Native compiled-source snapshot is missing or changed'
    disk = Path(receipt['ImagePath'])
    assert disk.exists() and digest(disk) == receipt['ImageSha256'], 'Native disk image differs from the accepted image hash'
    if not allow_failed:
        assert receipt.get('Passed') is True and receipt.get('ImageUnchanged') is True and receipt.get('QemuExitCode') == 0, 'Native acceptance failed; use --AllowFailedNative only for an explicitly failed diagnostic comparison'
    image_hash = digest(path)
    matching = [entry for entry in find_screenshots(receipt) if Path(entry['Path']).resolve() == path.resolve() and entry['Sha256'] == image_hash]
    assert matching, 'Native screenshot path/hash does not appear in the QEMU receipt'
    with Image.open(path) as image:
        assert image.size == (receipt['NativeWidth'], receipt['NativeHeight']), 'Native image was resized or cropped before comparison'
    return {'ReceiptPath': str(receipt_path.resolve()), 'ReceiptSha256': digest(receipt_path),
            'ScreenshotPath': str(path.resolve()), 'ScreenshotSha256': image_hash,
            'AcceptancePassed': receipt.get('Passed') is True,
            'KernelSha256': receipt['KernelSha256'], 'ImageSha256': receipt['ImageSha256'],
            'SourceSnapshotSha256': receipt['SourceSnapshotSha256'],
            'NativeWidth': receipt['NativeWidth'], 'NativeHeight': receipt['NativeHeight'],
            'MemoryMiB': receipt.get('MemoryMiB'), 'ScalePercent': receipt.get('ScalePercent'),
            'SourceKind': 'Actual QEMU native framebuffer screendump'}


def verify_reference(path, receipt_path):
    receipt = read(receipt_path)
    assert receipt.get('Kind') == 'WebsiteBrowserReferenceScreenshot', 'Reference screenshot requires explicit website-browser provenance'
    assert Path(receipt['ScreenshotPath']).resolve() == path.resolve(), 'Reference screenshot path differs from its receipt'
    assert receipt['ScreenshotSha256'] == digest(path), 'Reference screenshot hash differs from its receipt'
    assert len(receipt['WebsiteSourceManifestSha256']) == 64, 'Website source asset binding is absent'
    viewport = receipt['ViewportCss']
    assert viewport['Width'] > 0 and viewport['Height'] > 0 and 0 < viewport['DeviceScaleFactor'] <= 8
    with Image.open(path) as image:
        expected = (round(viewport['Width'] * viewport['DeviceScaleFactor']), round(viewport['Height'] * viewport['DeviceScaleFactor']))
        assert image.size == expected, ('Reference pixels differ from the declared viewport/device scale', image.size, expected)
    return {'ReceiptPath': str(receipt_path.resolve()), 'ReceiptSha256': digest(receipt_path),
            'ScreenshotPath': str(path.resolve()), 'ScreenshotSha256': digest(path),
            'WebsiteSourceManifestSha256': receipt['WebsiteSourceManifestSha256'], 'ViewportCss': viewport,
            'SourceKind': 'Host website browser reference; never native guest acceptance',
            'NativeGuestTested': False}


def roi(image, bounds):
    x, y, width, height = bounds
    assert min(x, y) >= 0 and min(width, height) > 0
    assert x + width <= image.width and y + height <= image.height, ('ROI exceeds original screenshot bounds', bounds, image.size)
    return image.crop((x, y, x + width, y + height)).convert('RGB')


def compare(native_path, native_receipt, reference_path, reference_receipt, native_roi, reference_roi, native_scale, reference_scale, output, allow_failed=False):
    assert not output.exists(), 'Refusing to overwrite an existing comparison directory'
    assert 0 < native_scale <= 8 and 0 < reference_scale <= 8
    native_evidence = verify_native(native_path, native_receipt, allow_failed)
    reference_evidence = verify_reference(reference_path, reference_receipt)
    assert math.isclose(native_scale * 100, native_evidence['ScalePercent']), 'Native comparison scale differs from guest UI scale'
    assert math.isclose(reference_scale, reference_evidence['ViewportCss']['DeviceScaleFactor']), 'Reference comparison scale differs from browser device scale'
    with Image.open(native_path) as native_original, Image.open(reference_path) as reference_original:
        native = roi(native_original, native_roi)
        reference = roi(reference_original, reference_roi)
    native_css = [native.width / native_scale, native.height / native_scale]
    reference_css = [reference.width / reference_scale, reference.height / reference_scale]
    assert all(math.isclose(a, b) for a, b in zip(native_css, reference_css)), ('ROI CSS sizes must match; no resize is performed', native_css, reference_css)
    eligible = native.size == reference.size
    result = {'Kind': 'ActualNativeAndWebsiteScreenshotComparison', 'ComparisonCompleted': True,
              'Native': native_evidence, 'WebsiteReference': reference_evidence,
              'NativeRoiPhysical': native_roi, 'ReferenceRoiPhysical': reference_roi,
              'NativeScale': native_scale, 'ReferenceScale': reference_scale,
              'ComparedCssSize': native_css, 'ComparisonEligible': eligible,
              'OriginalPixelsPreserved': True, 'ImagesResized': False,
              'HostImageCountsAsNativeProof': False,
              'Meaning': 'Numerical pixel differences are observations. Completion does not mean visual parity or native compatibility passed.'}
    output.mkdir(parents=True)
    (output / 'NativeReceipt.json').write_bytes(native_receipt.read_bytes())
    (output / 'ReferenceReceipt.json').write_bytes(reference_receipt.read_bytes())
    native.save(output / 'NativeRoi.png'); reference.save(output / 'ReferenceRoi.png')
    side = Image.new('RGB', (native.width + reference.width, max(native.height, reference.height) + 24), '#202020')
    side.paste(native, (0, 24)); side.paste(reference, (native.width, 24))
    labels = ImageDraw.Draw(side)
    labels.text((4, 5), 'QEMU native framebuffer', fill='white')
    labels.text((native.width + 4, 5), 'Website browser reference', fill='white')
    side.save(output / 'SideBySide.png')
    if eligible:
        difference = ImageChops.difference(native, reference)
        stats = ImageStat.Stat(difference)
        maximum = ImageChops.lighter(ImageChops.lighter(difference.getchannel(0), difference.getchannel(1)), difference.getchannel(2))
        unchanged = maximum.histogram()[0]
        exact = difference.getbbox() is None
        result.update({'ExactPixelMatch': exact, 'PixelParityAccepted': exact,
                       'MeanAbsoluteChannelError': sum(stats.mean) / 3,
                       'RootMeanSquareChannelError': math.sqrt(sum(value * value for value in stats.rms) / 3),
                       'ChannelMaximumError': [item[1] for item in stats.extrema],
                       'ChangedPixels': native.width * native.height - unchanged,
                       'ChangedPixelRatio': 1 - unchanged / (native.width * native.height)})
        difference.save(output / 'Difference.png')
        difference.point(lambda value: min(255, value * 4)).save(output / 'DifferenceAmplified4x.png')
    else:
        result.update({'ExactPixelMatch': None, 'PixelParityAccepted': False,
                       'Reason': 'CSS viewport matches but physical pixels differ because device scales differ; no resampling was used to mask the difference.'})
    (output / 'Result.json').write_text(json.dumps(result, ensure_ascii=False, indent=2) + '\n', 'utf-8')
    return result


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--Native', type=Path, required=True)
    parser.add_argument('--NativeReceipt', type=Path, required=True)
    parser.add_argument('--Reference', type=Path, required=True)
    parser.add_argument('--ReferenceReceipt', type=Path, required=True)
    parser.add_argument('--NativeRoi', type=int, nargs=4, required=True, metavar=('X', 'Y', 'WIDTH', 'HEIGHT'))
    parser.add_argument('--ReferenceRoi', type=int, nargs=4, required=True, metavar=('X', 'Y', 'WIDTH', 'HEIGHT'))
    parser.add_argument('--NativeScale', type=float, required=True)
    parser.add_argument('--ReferenceScale', type=float, required=True)
    parser.add_argument('--Output', type=Path, required=True)
    parser.add_argument('--AllowFailedNative', action='store_true')
    args = parser.parse_args()
    result = compare(args.Native, args.NativeReceipt, args.Reference, args.ReferenceReceipt,
                     args.NativeRoi, args.ReferenceRoi, args.NativeScale, args.ReferenceScale, args.Output, args.AllowFailedNative)
    print(json.dumps({'Output': str(args.Output.resolve()), 'ComparisonEligible': result['ComparisonEligible'],
                      'ExactPixelMatch': result['ExactPixelMatch'], 'MeanAbsoluteChannelError': result.get('MeanAbsoluteChannelError')}))


if __name__ == '__main__':
    main()