feat(macos): enable meeting integrations
PR and Push Build/Test / build-and-test (pull_request) Failing after 19m49s

This commit is contained in:
dh
2026-08-06 08:19:26 +02:00
parent 9542c05ea4
commit 2af7933a1b
27 changed files with 1999 additions and 227 deletions
@@ -2,8 +2,6 @@ using System.Net;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Drawing;
using System.Drawing.Imaging;
using MeetingAssistant.Screenshots;
using Microsoft.Extensions.Logging.Abstractions;
@@ -284,15 +282,7 @@ public sealed class LiteLlmScreenshotOcrClientTests
private static byte[] CreatePngBytes(int width, int height)
{
using var bitmap = new Bitmap(width, height);
using (var graphics = Graphics.FromImage(bitmap))
{
graphics.Clear(Color.White);
}
using var stream = new MemoryStream();
bitmap.Save(stream, ImageFormat.Png);
return stream.ToArray();
return PngTestFixture.Solid(width, height);
}
private static string CreateNonStreamingTextResponse(string text)
@@ -0,0 +1,436 @@
using System.Net.Http.Json;
using MeetingAssistant.Calendar;
using MeetingAssistant.MacOs;
using MeetingAssistant.MeetingNotes;
using MeetingAssistant.Screenshots;
using MeetingAssistant.Transcription;
using MeetingAssistant.Workflow;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
namespace MeetingAssistant.Tests;
public sealed class MacOsMeetingIntegrationTests
{
[Fact]
public void PortableRegistrationUsesRealMacOsProvidersOnlyOnMacOs()
{
var macServices = new ServiceCollection().AddLogging();
var unsupportedServices = new ServiceCollection().AddLogging();
MacOsMeetingIntegrationRegistration.Add(macServices, isMacOs: true);
MacOsMeetingIntegrationRegistration.Add(unsupportedServices, isMacOs: false);
using var macProvider = macServices.BuildServiceProvider();
using var unsupportedProvider = unsupportedServices.BuildServiceProvider();
Assert.IsType<MacOsMeetingMetadataProvider>(
macProvider.GetRequiredService<IMeetingMetadataProvider>());
Assert.IsType<MacOsCalendarMeetingProvider>(
macProvider.GetRequiredService<ICalendarMeetingProvider>());
Assert.IsType<MacOsMeetingStartPromptService>(
macProvider.GetRequiredService<IMeetingStartPromptService>());
Assert.IsType<MacOsDesktopIntegrationClient>(
macProvider.GetRequiredService<IActiveWindowScreenshotCapture>());
Assert.IsType<MacOsDesktopIntegrationClient>(
macProvider.GetRequiredService<IScreenshotImageCropper>());
Assert.IsType<NoopMeetingMetadataProvider>(
unsupportedProvider.GetRequiredService<IMeetingMetadataProvider>());
Assert.IsType<NoopCalendarMeetingProvider>(
unsupportedProvider.GetRequiredService<ICalendarMeetingProvider>());
Assert.IsType<NoopMeetingStartPromptService>(
unsupportedProvider.GetRequiredService<IMeetingStartPromptService>());
Assert.IsType<UnavailableActiveWindowScreenshotCapture>(
unsupportedProvider.GetRequiredService<IActiveWindowScreenshotCapture>());
Assert.IsType<UnavailableScreenshotImageCropper>(
unsupportedProvider.GetRequiredService<IScreenshotImageCropper>());
}
[Fact]
public async Task MacOsCalendarProvidersReturnTeamsMetadataAndPromptCandidate()
{
var startedAt = new DateTimeOffset(2026, 7, 25, 10, 5, 0, TimeSpan.FromHours(2));
var calendarEvent = new MacOsCalendarEvent(
"event-1",
"Project sync",
startedAt.AddMinutes(-5),
startedAt.AddMinutes(55),
"Microsoft Teams",
"Discuss launch readiness.\nJoin Microsoft Teams Meeting",
"https://teams.microsoft.com/l/meetup-join/example",
["Ada Lovelace <ada@example.com>", "Grace Hopper <grace@example.com>"],
IsCanceled: false);
var client = new StubMacOsDesktopIntegrationClient(calendarEvent);
IMeetingMetadataProvider metadataProvider = new MacOsMeetingMetadataProvider(
client,
NullLogger<MacOsMeetingMetadataProvider>.Instance);
ICalendarMeetingProvider calendarProvider = new MacOsCalendarMeetingProvider(
client,
NullLogger<MacOsCalendarMeetingProvider>.Instance);
var metadata = await metadataProvider.GetCurrentMeetingAsync(
startedAt,
CancellationToken.None);
var candidates = await calendarProvider.GetRecordingPromptCandidatesForDayAsync(
DateOnly.FromDateTime(startedAt.Date),
CancellationToken.None);
Assert.NotNull(metadata);
Assert.Equal("Project sync", metadata.Title);
Assert.Equal(
["Ada Lovelace <ada@example.com>", "Grace Hopper <grace@example.com>"],
metadata.Attendees);
Assert.Equal("Discuss launch readiness.", metadata.Agenda);
Assert.Equal(calendarEvent.End, metadata.ScheduledEnd);
var candidate = Assert.Single(candidates);
Assert.Equal("event-1", candidate.Id);
Assert.Equal("Project sync", candidate.Subject);
Assert.NotNull(candidate.Metadata);
Assert.Equal(metadata.Title, candidate.Metadata.Title);
Assert.Equal(metadata.Attendees, candidate.Metadata.Attendees);
Assert.Equal(metadata.Agenda, candidate.Metadata.Agenda);
Assert.Equal(metadata.ScheduledEnd, candidate.Metadata.ScheduledEnd);
}
[Fact]
public async Task MacOsPromptAndScreenshotUseNativeClientResults()
{
var screenshot = new ActiveWindowScreenshot([137, 80, 78, 71], "Meeting deck");
var client = new StubMacOsDesktopIntegrationClient(
screenshot: screenshot,
promptResponse: MeetingStartPromptResponse.Record);
IMeetingStartPromptService prompt = new MacOsMeetingStartPromptService(
client,
NullLogger<MacOsMeetingStartPromptService>.Instance);
IActiveWindowScreenshotCapture capture = client;
IScreenshotImageCropper cropper = client;
MeetingStartPromptResponse? callbackResponse = null;
var request = new MeetingStartPromptRequest(new CalendarMeeting(
"event-1",
"Project sync",
DateTimeOffset.Now,
DateTimeOffset.Now.AddHours(1)));
await prompt.ShowPromptAsync(
request,
(response, _) =>
{
callbackResponse = response;
return Task.CompletedTask;
},
CancellationToken.None);
var captured = await capture.CaptureAsync(CancellationToken.None);
var croppedPath = await cropper.SaveCroppedScreenshotAsync(
"meeting.png",
new ScreenshotCropCoordinates(1, 2, 3, 4),
CancellationToken.None);
Assert.Equal(MeetingStartPromptResponse.Record, callbackResponse);
Assert.Equal(screenshot, captured);
Assert.Equal("meeting.png.cropped.png", croppedPath);
}
[Fact]
public async Task MacOsCapabilityEndpointReportsEnabledRealProviders()
{
if (!OperatingSystem.IsMacOS())
{
return;
}
await using var factory = new WebApplicationFactory<Program>();
using var client = factory.CreateClient();
var report = await client.GetFromJsonAsync<PlatformCapabilityReport>(
"/diagnostics/platform-capabilities");
Assert.NotNull(report);
Assert.Equal("macOS", report.Host);
Assert.Equal(nameof(MacOsMeetingMetadataProvider), report.MeetingMetadataProvider);
Assert.Equal(nameof(MacOsCalendarMeetingProvider), report.CalendarMeetingProvider);
Assert.Equal(nameof(MacOsMeetingStartPromptService), report.MeetingStartPromptProvider);
Assert.Equal(nameof(MacOsDesktopIntegrationClient), report.ScreenshotProvider);
Assert.Equal(nameof(MacOsWorkflowRulesEditorWindowService), report.WorkflowEditorProvider);
Assert.True(report.WorkflowEditorAvailable);
Assert.True(report.CalendarRecordingPromptsEnabled);
Assert.True(report.SpeakerIdentificationEnabled);
Assert.True(report.ScreenshotOcrEnabled);
Assert.True(report.FunAsrBackendEnabled);
Assert.True(report.FunAsrDiarizationEnabled);
Assert.True(report.WhisperLocalDiarizationEnabled);
}
[Fact]
public async Task NativeHelperAdvertisesCalendarPromptAndScreenshotFeatures()
{
if (!OperatingSystem.IsMacOS())
{
return;
}
var runner = new ProcessCommandRunner(
NullLogger<ProcessCommandRunner>.Instance);
var result = await runner.RunAsync(
GetNativeHelperPath(),
["self-test"],
CancellationToken.None);
Assert.Equal(0, result.ExitCode);
using var json = System.Text.Json.JsonDocument.Parse(result.StandardOutput);
Assert.Equal("ok", json.RootElement.GetProperty("status").GetString());
Assert.Equal(
["calendar", "prompt", "screenshot", "crop"],
json.RootElement.GetProperty("features")
.EnumerateArray()
.Select(feature => feature.GetString()!)
.ToArray());
}
[Fact]
public async Task NativeHelperCropsPngUsingOcrPixelCoordinates()
{
if (!OperatingSystem.IsMacOS())
{
return;
}
var testRoot = Path.Combine(
Path.GetTempPath(),
$"meeting-assistant-native-crop-{Guid.NewGuid():N}");
Directory.CreateDirectory(testRoot);
var bitmapPath = Path.Combine(testRoot, "input.bmp");
var inputPath = Path.Combine(testRoot, "input.png");
var outputPath = Path.Combine(testRoot, "output.png");
var outputBitmapPath = Path.Combine(testRoot, "output.bmp");
try
{
await File.WriteAllBytesAsync(
bitmapPath,
CreateTopRedBottomBlueBitmap());
var runner = new ProcessCommandRunner(
NullLogger<ProcessCommandRunner>.Instance);
var inputConversion = await runner.RunAsync(
"/usr/bin/sips",
["-s", "format", "png", bitmapPath, "--out", inputPath],
CancellationToken.None);
Assert.Equal(0, inputConversion.ExitCode);
var result = await runner.RunAsync(
GetNativeHelperPath(),
["crop", inputPath, outputPath, "0", "0", "2", "1"],
CancellationToken.None);
Assert.Equal(0, result.ExitCode);
var png = await File.ReadAllBytesAsync(outputPath);
Assert.Equal((2, 1), PngTestFixture.ReadDimensions(png));
var outputConversion = await runner.RunAsync(
"/usr/bin/sips",
["-s", "format", "bmp", outputPath, "--out", outputBitmapPath],
CancellationToken.None);
Assert.Equal(0, outputConversion.ExitCode);
Assert.Equal(
(Red: (byte)255, Green: (byte)0, Blue: (byte)0),
ReadFirstBitmapPixel(await File.ReadAllBytesAsync(outputBitmapPath)));
}
finally
{
Directory.Delete(testRoot, recursive: true);
}
}
[Fact]
public async Task CalendarProviderUsesActualLocalMidnightsAcrossDaylightSavingTransition()
{
if (TimeZoneInfo.Local.Id != "Europe/Berlin")
{
return;
}
var client = new StubMacOsDesktopIntegrationClient();
var provider = new MacOsCalendarMeetingProvider(
client,
NullLogger<MacOsCalendarMeetingProvider>.Instance);
await provider.GetRecordingPromptCandidatesForDayAsync(
new DateOnly(2026, 3, 29),
CancellationToken.None);
Assert.Equal(
DateTimeOffset.Parse("2026-03-29T00:00:00+01:00"),
client.CalendarFrom);
Assert.Equal(
DateTimeOffset.Parse("2026-03-30T00:00:00+02:00"),
client.CalendarTo);
Assert.Equal(TimeSpan.FromHours(23), client.CalendarTo - client.CalendarFrom);
}
[Fact]
public async Task CalendarClientFallsBackToCalendarAutomationWhenEventKitIsDenied()
{
if (!OperatingSystem.IsMacOS())
{
return;
}
var runner = new SequenceCommandRunner(
new CommandResult(
MacOsIntegrationException.CalendarPermissionDeniedExitCode,
"",
"Calendar Full Access permission is required."),
new CommandResult(
0,
"""
[{"id":"fallback-event","subject":"Project sync","start":"2026-07-25T08:00:00Z","end":"2026-07-25T09:00:00Z","location":"Microsoft Teams","notes":"","url":"https://teams.microsoft.com/example","attendees":[],"isCanceled":false}]
""",
""));
IMacOsCalendarClient client = new MacOsDesktopIntegrationClient(
runner,
Path.GetDirectoryName(GetNativeHelperPath())!);
var events = await client.GetCalendarEventsAsync(
DateTimeOffset.Parse("2026-07-25T00:00:00Z"),
DateTimeOffset.Parse("2026-07-26T00:00:00Z"),
CancellationToken.None);
var calendarEvent = Assert.Single(events);
Assert.Equal("fallback-event", calendarEvent.Id);
Assert.Collection(
runner.Commands,
command => Assert.Equal("calendar", command.Arguments[0]),
command =>
{
Assert.Equal("/usr/bin/osascript", command.FileName);
Assert.Equal(["-l", "JavaScript"], command.Arguments.Take(2));
});
}
private static string GetNativeHelperPath()
{
var configuration = new DirectoryInfo(AppContext.BaseDirectory)
.Parent?.Name ?? "Debug";
return Path.GetFullPath(Path.Combine(
AppContext.BaseDirectory,
"..",
"..",
"..",
"..",
"MeetingAssistant",
"bin",
configuration,
"net10.0",
"Native",
"macos-meeting-integrations"));
}
private static byte[] CreateTopRedBottomBlueBitmap()
{
var bitmap = new byte[70];
bitmap[0] = (byte)'B';
bitmap[1] = (byte)'M';
BitConverter.GetBytes(bitmap.Length).CopyTo(bitmap, 2);
BitConverter.GetBytes(54).CopyTo(bitmap, 10);
BitConverter.GetBytes(40).CopyTo(bitmap, 14);
BitConverter.GetBytes(2).CopyTo(bitmap, 18);
BitConverter.GetBytes(2).CopyTo(bitmap, 22);
BitConverter.GetBytes((short)1).CopyTo(bitmap, 26);
BitConverter.GetBytes((short)24).CopyTo(bitmap, 28);
BitConverter.GetBytes(16).CopyTo(bitmap, 34);
// BMP rows are bottom-up and padded to four-byte boundaries.
bitmap[54] = 255;
bitmap[57] = 255;
bitmap[64] = 255;
bitmap[67] = 255;
return bitmap;
}
private static (byte Red, byte Green, byte Blue) ReadFirstBitmapPixel(byte[] bitmap)
{
var pixelOffset = BitConverter.ToInt32(bitmap, 10);
return (
Red: bitmap[pixelOffset + 2],
Green: bitmap[pixelOffset + 1],
Blue: bitmap[pixelOffset]);
}
private sealed class StubMacOsDesktopIntegrationClient :
IMacOsCalendarClient,
IMacOsMeetingPromptClient,
IActiveWindowScreenshotCapture,
IScreenshotImageCropper
{
private readonly IReadOnlyList<MacOsCalendarEvent> events;
private readonly ActiveWindowScreenshot screenshot;
private readonly MeetingStartPromptResponse promptResponse;
public StubMacOsDesktopIntegrationClient(
MacOsCalendarEvent? calendarEvent = null,
ActiveWindowScreenshot? screenshot = null,
MeetingStartPromptResponse promptResponse = MeetingStartPromptResponse.Skip)
{
events = calendarEvent is null ? [] : [calendarEvent];
this.screenshot = screenshot ?? new ActiveWindowScreenshot([], null);
this.promptResponse = promptResponse;
}
public Task<IReadOnlyList<MacOsCalendarEvent>> GetCalendarEventsAsync(
DateTimeOffset from,
DateTimeOffset to,
CancellationToken cancellationToken)
{
CalendarFrom = from;
CalendarTo = to;
return Task.FromResult(events);
}
public DateTimeOffset CalendarFrom { get; private set; }
public DateTimeOffset CalendarTo { get; private set; }
public Task<MeetingStartPromptResponse> ShowMeetingStartPromptAsync(
MeetingStartPromptRequest request,
CancellationToken cancellationToken)
{
return Task.FromResult(promptResponse);
}
public Task<ActiveWindowScreenshot> CaptureAsync(CancellationToken cancellationToken)
{
return Task.FromResult(screenshot);
}
public Task<string?> SaveCroppedScreenshotAsync(
string screenshotPath,
ScreenshotCropCoordinates crop,
CancellationToken cancellationToken)
{
return Task.FromResult<string?>($"{screenshotPath}.cropped.png");
}
}
private sealed class SequenceCommandRunner : ICommandRunner
{
private readonly Queue<CommandResult> results;
public SequenceCommandRunner(params CommandResult[] results)
{
this.results = new Queue<CommandResult>(results);
}
public List<(string FileName, IReadOnlyList<string> Arguments)> Commands { get; } = [];
public Task<CommandResult> RunAsync(
string fileName,
IReadOnlyList<string> arguments,
CancellationToken cancellationToken,
IReadOnlyDictionary<string, string>? environment = null)
{
Commands.Add((fileName, arguments));
return Task.FromResult(results.Dequeue());
}
}
}
@@ -4,8 +4,6 @@ using MeetingAssistant.Speakers;
using MeetingAssistant.Workflow;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using System.Drawing;
using System.Drawing.Imaging;
#pragma warning disable CA1416
@@ -117,11 +115,7 @@ public sealed class MeetingScreenshotServiceTests
var croppedPath = Assert.Single(Directory.GetFiles(
Path.Combine(Path.GetDirectoryName(fixture.Artifacts.AssistantContextPath)!, "Attachments"),
"*-cropped.png"));
using (var cropped = new Bitmap(croppedPath))
{
Assert.Equal(2, cropped.Width);
Assert.Equal(2, cropped.Height);
}
Assert.Equal((2, 2), PngTestFixture.ReadDimensions(await File.ReadAllBytesAsync(croppedPath)));
var context = await File.ReadAllTextAsync(fixture.Artifacts.AssistantContextPath);
var screenshotIndex = context.IndexOf("![Screenshot 00:00:10]", StringComparison.Ordinal);
@@ -133,6 +127,38 @@ public sealed class MeetingScreenshotServiceTests
Assert.Contains("Shared screen text", context);
}
[Fact]
public async Task CaptureKeepsSuccessfulOcrTextWhenCropperRejectsCoordinates()
{
var fixture = await ScreenshotFixture.CreateAsync(options =>
{
options.Screenshots.Ocr.Enabled = true;
});
var ocr = new CapturingScreenshotOcrClient(
"Readable text survives.",
new ScreenshotCropCoordinates(99, 99, 10, 10));
var service = fixture.CreateService(
new FixedScreenshotCapture(CreatePngBytes(4, 4)),
ocr,
screenshotImageCropper: new RejectingScreenshotImageCropper());
await service.CaptureAsync(
fixture.Artifacts,
DateTimeOffset.Parse("2026-05-26T10:00:00+02:00"),
DateTimeOffset.Parse("2026-05-26T10:00:10+02:00"),
fixture.Options,
CancellationToken.None);
await service.WaitForPendingOcrAsync(
fixture.Artifacts,
TimeSpan.FromSeconds(5),
CancellationToken.None);
var context = await File.ReadAllTextAsync(fixture.Artifacts.AssistantContextPath);
Assert.Contains("Readable text survives.", context);
Assert.DoesNotContain("_OCR failed", context);
Assert.DoesNotContain("![Cropped screenshot]", context);
}
[Fact]
public async Task CaptureAddsOcrAttendeesToMeetingNoteThroughCanonicalizer()
{
@@ -454,7 +480,8 @@ public sealed class MeetingScreenshotServiceTests
IActiveWindowScreenshotCapture capture,
IScreenshotOcrClient ocrClient,
ISpeakerIdentityAttendeeCanonicalizer? attendeeCanonicalizer = null,
IMeetingWorkflowEngine? meetingWorkflowEngine = null)
IMeetingWorkflowEngine? meetingWorkflowEngine = null,
IScreenshotImageCropper? screenshotImageCropper = null)
{
return new MeetingScreenshotService(
capture,
@@ -462,12 +489,28 @@ public sealed class MeetingScreenshotServiceTests
NoteStore,
attendeeCanonicalizer ?? PassthroughSpeakerIdentityAttendeeCanonicalizer.Instance,
ocrClient,
screenshotImageCropper ??
capture as IScreenshotImageCropper ??
new UnavailableScreenshotImageCropper(),
NullLogger<MeetingScreenshotService>.Instance,
meetingWorkflowEngine);
}
}
private sealed class FixedScreenshotCapture : IActiveWindowScreenshotCapture
private sealed class RejectingScreenshotImageCropper : IScreenshotImageCropper
{
public Task<string?> SaveCroppedScreenshotAsync(
string screenshotPath,
ScreenshotCropCoordinates crop,
CancellationToken cancellationToken)
{
throw new InvalidDataException("The crop is outside image bounds.");
}
}
private sealed class FixedScreenshotCapture :
IActiveWindowScreenshotCapture,
IScreenshotImageCropper
{
private readonly byte[] bytes;
@@ -480,6 +523,21 @@ public sealed class MeetingScreenshotServiceTests
{
return Task.FromResult(new ActiveWindowScreenshot(bytes, "Demo Window"));
}
public async Task<string?> SaveCroppedScreenshotAsync(
string screenshotPath,
ScreenshotCropCoordinates crop,
CancellationToken cancellationToken)
{
var croppedPath = Path.Combine(
Path.GetDirectoryName(screenshotPath)!,
$"{Path.GetFileNameWithoutExtension(screenshotPath)}-cropped.png");
await File.WriteAllBytesAsync(
croppedPath,
PngTestFixture.Solid(2, 2),
cancellationToken);
return croppedPath;
}
}
private sealed class CapturingScreenshotOcrClient : IScreenshotOcrClient
@@ -614,15 +672,7 @@ public sealed class MeetingScreenshotServiceTests
private static byte[] CreatePngBytes(int width, int height)
{
using var bitmap = new Bitmap(width, height);
using (var graphics = Graphics.FromImage(bitmap))
{
graphics.Clear(Color.White);
}
using var stream = new MemoryStream();
bitmap.Save(stream, ImageFormat.Png);
return stream.ToArray();
return PngTestFixture.Solid(width, height);
}
private static string ExtractScreenshotOcrId(string context)
+38
View File
@@ -0,0 +1,38 @@
namespace MeetingAssistant.Tests;
internal static class PngTestFixture
{
private const string Solid2X2 =
"iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAAXNSR0IArs4c6QAAADhlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAAqACAAQAAAABAAAAAqADAAQAAAABAAAAAgAAAADO0J6QAAAAEklEQVQIHWP8DwQMQMAEIkAAAD34BACALvQ5AAAAAElFTkSuQmCC";
private const string Solid4X4 =
"iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAYAAACp8Z5+AAAAAXNSR0IArs4c6QAAADhlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAAqACAAQAAAABAAAABKADAAQAAAABAAAABAAAAADmpNw4AAAAFUlEQVQIHWP8DwQMSIAJiQ1mEhYAAAZdBAQjjGwcAAAAAElFTkSuQmCC";
private const string Solid8X6 =
"iVBORw0KGgoAAAANSUhEUgAAAAgAAAAGCAYAAAD+Bd/7AAAAAXNSR0IArs4c6QAAADhlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAAqACAAQAAAABAAAACKADAAQAAAABAAAABgAAAAAJfOcJAAAAFklEQVQIHWP8DwQMeAATHjmw1GBQAAAQiQQIVmhjoQAAAABJRU5ErkJggg==";
public static byte[] Solid(int width, int height)
{
var encoded = (width, height) switch
{
(2, 2) => Solid2X2,
(4, 4) => Solid4X4,
(8, 6) => Solid8X6,
_ => throw new ArgumentOutOfRangeException(
nameof(width),
$"No deterministic PNG fixture exists for {width}x{height}.")
};
return Convert.FromBase64String(encoded);
}
public static (int Width, int Height) ReadDimensions(byte[] png)
{
if (!png.AsSpan(0, 8).SequenceEqual(
new byte[] { 137, 80, 78, 71, 13, 10, 26, 10 }))
{
throw new InvalidDataException("The fixture is not a PNG.");
}
return (
System.Buffers.Binary.BinaryPrimitives.ReadInt32BigEndian(png.AsSpan(16, 4)),
System.Buffers.Binary.BinaryPrimitives.ReadInt32BigEndian(png.AsSpan(20, 4)));
}
}