From ea1baea99b155aa2a054f3f630fcd0da1687af2c Mon Sep 17 00:00:00 2001 From: Shaun Watson Date: Wed, 4 Mar 2026 09:54:20 +0000 Subject: [PATCH 1/6] Support non-seekable streams in FromStream method If the input stream is not seekable, buffer its contents into a MemoryStream to ensure compatibility with downstream processing that requires seeking. --- src/PdqHash/PdqHasher.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/PdqHash/PdqHasher.cs b/src/PdqHash/PdqHasher.cs index 99cdcab..3986702 100644 --- a/src/PdqHash/PdqHasher.cs +++ b/src/PdqHash/PdqHasher.cs @@ -65,8 +65,16 @@ private static void ComputeDCTMatrix(Memory memory) public HashResult? FromStream(Stream input, string source) { + if (!input.CanSeek) + { + using var buffered = new MemoryStream(); + input.CopyTo(buffered); + buffered.Position = 0; + return FromStream(buffered, source); + } + var stopwatch = Stopwatch.StartNew(); - + using var codec = SKCodec.Create(input, out var result); if (codec == null) From 46b16322d5b958dacc81f3234336dc24c68e315d Mon Sep 17 00:00:00 2001 From: Shaun Watson Date: Wed, 4 Mar 2026 10:13:49 +0000 Subject: [PATCH 2/6] Updated the write task to call CompleteAsync after writing bytes to the pipe. This signals to the reader that no more data will be written. --- test/PdqHash.Tests/Streams/StreamParsingTests.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/PdqHash.Tests/Streams/StreamParsingTests.cs b/test/PdqHash.Tests/Streams/StreamParsingTests.cs index 73abbea..219615a 100644 --- a/test/PdqHash.Tests/Streams/StreamParsingTests.cs +++ b/test/PdqHash.Tests/Streams/StreamParsingTests.cs @@ -62,7 +62,11 @@ public async Task EnsureHashingWithNonSeekableStreams() return hasher.FromStream(pipe.Reader.AsStream(), ""); }); - var writeTask = Task.Run(() => pipe.Writer.WriteAsync(bytes)); + var writeTask = Task.Run(async () => + { + await pipe.Writer.WriteAsync(bytes); + await pipe.Writer.CompleteAsync(); + }); await Task.WhenAll(hashTask, writeTask); From 7d86d69f54aa6e998dc7b1e3ff988c08ac421f91 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Mar 2026 10:50:05 +0000 Subject: [PATCH 3/6] Initial plan From 32171d35e9d5dfd744466ab35449a26a0def1f2d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Mar 2026 11:06:51 +0000 Subject: [PATCH 4/6] Include buffering time in ReadDuration for non-seekable streams Co-authored-by: iwillspeak <1004401+iwillspeak@users.noreply.github.com> --- src/PdqHash/PdqHasher.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/PdqHash/PdqHasher.cs b/src/PdqHash/PdqHasher.cs index 3986702..437f4d0 100644 --- a/src/PdqHash/PdqHasher.cs +++ b/src/PdqHash/PdqHasher.cs @@ -65,16 +65,16 @@ private static void ComputeDCTMatrix(Memory memory) public HashResult? FromStream(Stream input, string source) { - if (!input.CanSeek) + var stopwatch = Stopwatch.StartNew(); + + using var bufferedStream = input.CanSeek ? null : new MemoryStream(); + if (bufferedStream != null) { - using var buffered = new MemoryStream(); - input.CopyTo(buffered); - buffered.Position = 0; - return FromStream(buffered, source); + input.CopyTo(bufferedStream); + bufferedStream.Position = 0; + input = bufferedStream; } - var stopwatch = Stopwatch.StartNew(); - using var codec = SKCodec.Create(input, out var result); if (codec == null) From e8c50113c35011d9b522c3e27de4408286a898dd Mon Sep 17 00:00:00 2001 From: Shaun Watson Date: Wed, 4 Mar 2026 14:20:15 +0000 Subject: [PATCH 5/6] Allow PdqHash to Target .NET 9 & 10. --- src/PdqHash/Extensions/CachedAsyncEnumerable.cs | 6 ++++++ src/PdqHash/PdqHash.csproj | 2 +- src/PdqHash/PdqHash256.cs | 2 +- src/PdqHash/Video/VPdqComparer.cs | 4 ++-- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/PdqHash/Extensions/CachedAsyncEnumerable.cs b/src/PdqHash/Extensions/CachedAsyncEnumerable.cs index a35b9fb..81a4c6f 100644 --- a/src/PdqHash/Extensions/CachedAsyncEnumerable.cs +++ b/src/PdqHash/Extensions/CachedAsyncEnumerable.cs @@ -9,4 +9,10 @@ public class CachedAsyncEnumerable(IAsyncEnumerable asyncEnumerable) : IAs public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) => new CachedAsyncEnumerator(_materialized, _asyncEnumerator); + + /// + /// Returns the number of elements that have been materialized so far. + /// This is only accurate after the enumerable has been fully consumed. + /// + public int Count => _materialized.Count; } \ No newline at end of file diff --git a/src/PdqHash/PdqHash.csproj b/src/PdqHash/PdqHash.csproj index 692021e..8b0d72e 100644 --- a/src/PdqHash/PdqHash.csproj +++ b/src/PdqHash/PdqHash.csproj @@ -6,7 +6,7 @@ LICENSE README.md - net10.0 + net9.0;net10.0 enable enable true diff --git a/src/PdqHash/PdqHash256.cs b/src/PdqHash/PdqHash256.cs index 04db22f..31910f9 100644 --- a/src/PdqHash/PdqHash256.cs +++ b/src/PdqHash/PdqHash256.cs @@ -297,7 +297,7 @@ public string dumpBitsAcross() public string dumpWords() { - return string.Join(",", w.Reverse().Select(w => w.ToString(CultureInfo.InvariantCulture))); + return string.Join(",", Enumerable.Reverse(w).Select(w => w.ToString(CultureInfo.InvariantCulture))); } public int[] Words() => w.ToArray(); diff --git a/src/PdqHash/Video/VPdqComparer.cs b/src/PdqHash/Video/VPdqComparer.cs index eaee5ca..c6d753b 100644 --- a/src/PdqHash/Video/VPdqComparer.cs +++ b/src/PdqHash/Video/VPdqComparer.cs @@ -97,8 +97,8 @@ public static bool CalculateMatch(IReadOnlySet left, IReadOnlySet matchThreshold || rightMatchPercent > matchThreshold, leftMatchPercent, rightMatchPercent); } From f1b0684b313ad96342e8126c9d6f9028e82cee90 Mon Sep 17 00:00:00 2001 From: Shaun Watson Date: Thu, 5 Mar 2026 10:24:54 +0000 Subject: [PATCH 6/6] Revert "Allow PdqHash to Target .NET 9 & 10." This reverts commit e8c50113c35011d9b522c3e27de4408286a898dd. --- src/PdqHash/Extensions/CachedAsyncEnumerable.cs | 6 ------ src/PdqHash/PdqHash.csproj | 2 +- src/PdqHash/PdqHash256.cs | 2 +- src/PdqHash/Video/VPdqComparer.cs | 4 ++-- 4 files changed, 4 insertions(+), 10 deletions(-) diff --git a/src/PdqHash/Extensions/CachedAsyncEnumerable.cs b/src/PdqHash/Extensions/CachedAsyncEnumerable.cs index 81a4c6f..a35b9fb 100644 --- a/src/PdqHash/Extensions/CachedAsyncEnumerable.cs +++ b/src/PdqHash/Extensions/CachedAsyncEnumerable.cs @@ -9,10 +9,4 @@ public class CachedAsyncEnumerable(IAsyncEnumerable asyncEnumerable) : IAs public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) => new CachedAsyncEnumerator(_materialized, _asyncEnumerator); - - /// - /// Returns the number of elements that have been materialized so far. - /// This is only accurate after the enumerable has been fully consumed. - /// - public int Count => _materialized.Count; } \ No newline at end of file diff --git a/src/PdqHash/PdqHash.csproj b/src/PdqHash/PdqHash.csproj index 8b0d72e..692021e 100644 --- a/src/PdqHash/PdqHash.csproj +++ b/src/PdqHash/PdqHash.csproj @@ -6,7 +6,7 @@ LICENSE README.md - net9.0;net10.0 + net10.0 enable enable true diff --git a/src/PdqHash/PdqHash256.cs b/src/PdqHash/PdqHash256.cs index 31910f9..04db22f 100644 --- a/src/PdqHash/PdqHash256.cs +++ b/src/PdqHash/PdqHash256.cs @@ -297,7 +297,7 @@ public string dumpBitsAcross() public string dumpWords() { - return string.Join(",", Enumerable.Reverse(w).Select(w => w.ToString(CultureInfo.InvariantCulture))); + return string.Join(",", w.Reverse().Select(w => w.ToString(CultureInfo.InvariantCulture))); } public int[] Words() => w.ToArray(); diff --git a/src/PdqHash/Video/VPdqComparer.cs b/src/PdqHash/Video/VPdqComparer.cs index c6d753b..eaee5ca 100644 --- a/src/PdqHash/Video/VPdqComparer.cs +++ b/src/PdqHash/Video/VPdqComparer.cs @@ -97,8 +97,8 @@ public static bool CalculateMatch(IReadOnlySet left, IReadOnlySet matchThreshold || rightMatchPercent > matchThreshold, leftMatchPercent, rightMatchPercent); }