From f4cc295e4cd4495e6491a5eef4e9e5c7efc995ae Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 30 Jan 2025 12:15:08 +1000 Subject: [PATCH 01/18] Decode tile rows directly. Fix #2873 --- .../Formats/Tiff/TiffDecoderCore.cs | 35 +++++++++---------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs index 96e6d2a89d..f16ffd0da1 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs @@ -608,7 +608,7 @@ private void DecodeTilesPlanar( } /// - /// Decodes the image data for Tiff's which arrange the pixel data in tiles and the chunky configuration. + /// Decodes the image data for TIFFs which arrange the pixel data in tiles and the chunky configuration. /// /// The pixel format. /// The image frame to decode into. @@ -634,14 +634,10 @@ private void DecodeTilesChunky( int width = pixels.Width; int height = pixels.Height; int bitsPerPixel = this.BitsPerPixel; - - int bytesPerRow = RoundUpToMultipleOfEight(width * bitsPerPixel); int bytesPerTileRow = RoundUpToMultipleOfEight(tileWidth * bitsPerPixel); - int uncompressedTilesSize = bytesPerTileRow * tileLength; - using IMemoryOwner tileBuffer = this.memoryAllocator.Allocate(uncompressedTilesSize, AllocationOptions.Clean); - using IMemoryOwner uncompressedPixelBuffer = this.memoryAllocator.Allocate(tilesDown * tileLength * bytesPerRow, AllocationOptions.Clean); + + using IMemoryOwner tileBuffer = this.memoryAllocator.Allocate(bytesPerTileRow * tileLength, AllocationOptions.Clean); Span tileBufferSpan = tileBuffer.GetSpan(); - Span uncompressedPixelBufferSpan = uncompressedPixelBuffer.GetSpan(); using TiffBaseDecompressor decompressor = this.CreateDecompressor(frame.Width, bitsPerPixel); TiffBaseColorDecoder colorDecoder = this.CreateChunkyColorDecoder(); @@ -649,13 +645,15 @@ private void DecodeTilesChunky( int tileIndex = 0; for (int tileY = 0; tileY < tilesDown; tileY++) { - int remainingPixelsInRow = width; + int rowStartY = tileY * tileLength; + int rowEndY = Math.Min(rowStartY + tileLength, height); + for (int tileX = 0; tileX < tilesAcross; tileX++) { cancellationToken.ThrowIfCancellationRequested(); - int uncompressedPixelBufferOffset = tileY * tileLength * bytesPerRow; bool isLastHorizontalTile = tileX == tilesAcross - 1; + int remainingPixelsInRow = width - (tileX * tileWidth); decompressor.Decompress( this.inputStream, @@ -666,22 +664,23 @@ private void DecodeTilesChunky( cancellationToken); int tileBufferOffset = 0; - uncompressedPixelBufferOffset += bytesPerTileRow * tileX; - int bytesToCopy = isLastHorizontalTile ? RoundUpToMultipleOfEight(bitsPerPixel * remainingPixelsInRow) : bytesPerTileRow; - for (int y = 0; y < tileLength; y++) + for (int y = rowStartY; y < rowEndY; y++) { - Span uncompressedPixelRow = uncompressedPixelBufferSpan.Slice(uncompressedPixelBufferOffset, bytesToCopy); - tileBufferSpan.Slice(tileBufferOffset, bytesToCopy).CopyTo(uncompressedPixelRow); + int bytesToCopy = isLastHorizontalTile ? RoundUpToMultipleOfEight(bitsPerPixel * remainingPixelsInRow) : bytesPerTileRow; + ReadOnlySpan tileRowSpan = tileBufferSpan.Slice(tileBufferOffset, bytesToCopy); + + // Decode the tile row directly into the pixel buffer. + int left = tileX * tileWidth; + int rowWidth = Math.Min(tileWidth, remainingPixelsInRow); + + colorDecoder.Decode(tileRowSpan, pixels, left, y, rowWidth, 1); + tileBufferOffset += bytesPerTileRow; - uncompressedPixelBufferOffset += bytesPerRow; } - remainingPixelsInRow -= tileWidth; tileIndex++; } } - - colorDecoder.Decode(uncompressedPixelBufferSpan, pixels, 0, 0, width, height); } private TiffBaseColorDecoder CreateChunkyColorDecoder() From 80b336bd0f62ae758d786b7de867ac7a0005b4bb Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 30 Jan 2025 12:19:11 +1000 Subject: [PATCH 02/18] Optimize --- src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs index f16ffd0da1..18535d1e37 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs @@ -664,17 +664,15 @@ private void DecodeTilesChunky( cancellationToken); int tileBufferOffset = 0; + int bytesToCopy = isLastHorizontalTile ? RoundUpToMultipleOfEight(bitsPerPixel * remainingPixelsInRow) : bytesPerTileRow; + int rowWidth = Math.Min(tileWidth, remainingPixelsInRow); + int left = tileX * tileWidth; + for (int y = rowStartY; y < rowEndY; y++) { - int bytesToCopy = isLastHorizontalTile ? RoundUpToMultipleOfEight(bitsPerPixel * remainingPixelsInRow) : bytesPerTileRow; - ReadOnlySpan tileRowSpan = tileBufferSpan.Slice(tileBufferOffset, bytesToCopy); - // Decode the tile row directly into the pixel buffer. - int left = tileX * tileWidth; - int rowWidth = Math.Min(tileWidth, remainingPixelsInRow); - + ReadOnlySpan tileRowSpan = tileBufferSpan.Slice(tileBufferOffset, bytesToCopy); colorDecoder.Decode(tileRowSpan, pixels, left, y, rowWidth, 1); - tileBufferOffset += bytesPerTileRow; } From fcaef778a9924b07e4c0173fe678f2c3cd6b9f89 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 30 Jan 2025 13:25:24 +1000 Subject: [PATCH 03/18] Fix CI build errors. --- .github/workflows/build-and-test.yml | 2 +- .github/workflows/code-coverage.yml | 3 +++ src/ImageSharp/Formats/Png/PngThrowHelper.cs | 4 ++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index e17b8d724e..d0e2f91265 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -67,7 +67,7 @@ jobs: steps: - name: Install libgdi+, which is required for tests running on ubuntu - if: ${{ matrix.options.os == 'buildjet-4vcpu-ubuntu-2204-arm' }} + if: ${{ contains(matrix.options.os, 'ubuntu') }} run: sudo apt-get -y install libgdiplus libgif-dev libglib2.0-dev libcairo2-dev libtiff-dev libexif-dev - name: Git Config diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index e551afbd6d..b4038e520d 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -17,6 +17,9 @@ jobs: runs-on: ${{matrix.options.os}} steps: + - name: Install libgdi+, which is required for tests running on ubuntu + run: sudo apt-get -y install libgdiplus libgif-dev libglib2.0-dev libcairo2-dev libtiff-dev libexif-dev + - name: Git Config shell: bash run: | diff --git a/src/ImageSharp/Formats/Png/PngThrowHelper.cs b/src/ImageSharp/Formats/Png/PngThrowHelper.cs index 0552e9a79e..bb21f348f3 100644 --- a/src/ImageSharp/Formats/Png/PngThrowHelper.cs +++ b/src/ImageSharp/Formats/Png/PngThrowHelper.cs @@ -40,11 +40,11 @@ public static void ThrowInvalidImageContentException(string errorMessage, Except public static void ThrowInvalidChunkCrc(string chunkTypeName) => throw new InvalidImageContentException($"CRC Error. PNG {chunkTypeName} chunk is corrupt!"); [DoesNotReturn] - public static void ThrowInvalidParameter(object value, string message, [CallerArgumentExpression(nameof(value))] string name = "") + public static void ThrowInvalidParameter(object value, string message, [CallerArgumentExpression("value")] string name = "") => throw new NotSupportedException($"Invalid {name}. {message}. Was '{value}'."); [DoesNotReturn] - public static void ThrowInvalidParameter(object value1, object value2, string message, [CallerArgumentExpression(nameof(value1))] string name1 = "", [CallerArgumentExpression(nameof(value1))] string name2 = "") + public static void ThrowInvalidParameter(object value1, object value2, string message, [CallerArgumentExpression("value1")] string name1 = "", [CallerArgumentExpression("value2")] string name2 = "") => throw new NotSupportedException($"Invalid {name1} or {name2}. {message}. Was '{value1}' and '{value2}'."); [DoesNotReturn] From b88f789ca04e59f3d8c5a5ac3ec19c952b158c6d Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 30 Jan 2025 13:30:56 +1000 Subject: [PATCH 04/18] Don't use TF during build, just test it. --- ci-build.ps1 | 5 ++++- src/ImageSharp/Formats/Png/PngThrowHelper.cs | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/ci-build.ps1 b/ci-build.ps1 index d45af6ff4d..d97cbbcb80 100644 --- a/ci-build.ps1 +++ b/ci-build.ps1 @@ -8,4 +8,7 @@ dotnet clean -c Release $repositoryUrl = "https://github.com/$env:GITHUB_REPOSITORY" # Building for a specific framework. -dotnet build -c Release -f $targetFramework /p:RepositoryUrl=$repositoryUrl +# dotnet build -c Release -f $targetFramework /p:RepositoryUrl=$repositoryUrl +# +# CI is now throwing build errors when none were present previously. +dotnet build -c Release -f /p:RepositoryUrl=$repositoryUrl diff --git a/src/ImageSharp/Formats/Png/PngThrowHelper.cs b/src/ImageSharp/Formats/Png/PngThrowHelper.cs index bb21f348f3..8dc70e1d9a 100644 --- a/src/ImageSharp/Formats/Png/PngThrowHelper.cs +++ b/src/ImageSharp/Formats/Png/PngThrowHelper.cs @@ -40,11 +40,11 @@ public static void ThrowInvalidImageContentException(string errorMessage, Except public static void ThrowInvalidChunkCrc(string chunkTypeName) => throw new InvalidImageContentException($"CRC Error. PNG {chunkTypeName} chunk is corrupt!"); [DoesNotReturn] - public static void ThrowInvalidParameter(object value, string message, [CallerArgumentExpression("value")] string name = "") + public static void ThrowInvalidParameter(object value, string message, [CallerArgumentExpression(nameof(value))] string name = "") => throw new NotSupportedException($"Invalid {name}. {message}. Was '{value}'."); [DoesNotReturn] - public static void ThrowInvalidParameter(object value1, object value2, string message, [CallerArgumentExpression("value1")] string name1 = "", [CallerArgumentExpression("value2")] string name2 = "") + public static void ThrowInvalidParameter(object value1, object value2, string message, [CallerArgumentExpression(nameof(value1))] string name1 = "", [CallerArgumentExpression(nameof(value2))] string name2 = "") => throw new NotSupportedException($"Invalid {name1} or {name2}. {message}. Was '{value1}' and '{value2}'."); [DoesNotReturn] From 09ca511cccddabc1210d77e88a2eab5cc03bc56b Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 30 Jan 2025 14:14:01 +1000 Subject: [PATCH 05/18] Try using setup-dotnet v4 --- .github/workflows/build-and-test.yml | 4 ++-- .github/workflows/code-coverage.yml | 2 +- ci-build.ps1 | 5 +---- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index d0e2f91265..5cf6d98068 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -109,14 +109,14 @@ jobs: - name: DotNet Setup if: ${{ matrix.options.sdk-preview != true }} - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: dotnet-version: | 6.0.x - name: DotNet Setup Preview if: ${{ matrix.options.sdk-preview == true }} - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: dotnet-version: | 7.0.x diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index b4038e520d..26e2013320 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -58,7 +58,7 @@ jobs: restore-keys: ${{ runner.os }}-nuget- - name: DotNet Setup - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: dotnet-version: | 6.0.x diff --git a/ci-build.ps1 b/ci-build.ps1 index d97cbbcb80..d45af6ff4d 100644 --- a/ci-build.ps1 +++ b/ci-build.ps1 @@ -8,7 +8,4 @@ dotnet clean -c Release $repositoryUrl = "https://github.com/$env:GITHUB_REPOSITORY" # Building for a specific framework. -# dotnet build -c Release -f $targetFramework /p:RepositoryUrl=$repositoryUrl -# -# CI is now throwing build errors when none were present previously. -dotnet build -c Release -f /p:RepositoryUrl=$repositoryUrl +dotnet build -c Release -f $targetFramework /p:RepositoryUrl=$repositoryUrl From b350dbe74870ee9d1e77fd68be1ca76d72dba659 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 30 Jan 2025 14:31:25 +1000 Subject: [PATCH 06/18] Try adding sdk install path to the global path --- .github/workflows/build-and-test.yml | 6 ++++++ .github/workflows/code-coverage.yml | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 5cf6d98068..c874fffc62 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -121,6 +121,12 @@ jobs: dotnet-version: | 7.0.x + - name: Ensure Dotnet Path + run: echo "/usr/share/dotnet" >> $GITHUB_PATH + + - name: Confirm .NET Version + run: dotnet --version # This will now find dotnet in /usr/share/dotnet + - name: DotNet Build if: ${{ matrix.options.sdk-preview != true }} shell: pwsh diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index 26e2013320..7fdb4bc826 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -63,6 +63,12 @@ jobs: dotnet-version: | 6.0.x + - name: Ensure Dotnet Path + run: echo "/usr/share/dotnet" >> $GITHUB_PATH + + - name: Confirm .NET Version + run: dotnet --version # This will now find dotnet in /usr/share/dotnet + - name: DotNet Build shell: pwsh run: ./ci-build.ps1 "${{matrix.options.framework}}" From 80a1432d997398ec2ceae776303e4cf252033d90 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 30 Jan 2025 14:39:16 +1000 Subject: [PATCH 07/18] Check the version within the powershell script --- .github/workflows/build-and-test.yml | 4 ++-- .github/workflows/code-coverage.yml | 4 ++-- ci-build.ps1 | 7 +++++++ 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index c874fffc62..1edb5ae81e 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -121,10 +121,10 @@ jobs: dotnet-version: | 7.0.x - - name: Ensure Dotnet Path + - name: Ensure DotNet Path run: echo "/usr/share/dotnet" >> $GITHUB_PATH - - name: Confirm .NET Version + - name: Confirm DotNet Version run: dotnet --version # This will now find dotnet in /usr/share/dotnet - name: DotNet Build diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index 7fdb4bc826..da7d792cd8 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -63,10 +63,10 @@ jobs: dotnet-version: | 6.0.x - - name: Ensure Dotnet Path + - name: Ensure DotNet Path run: echo "/usr/share/dotnet" >> $GITHUB_PATH - - name: Confirm .NET Version + - name: Confirm DotNet Version run: dotnet --version # This will now find dotnet in /usr/share/dotnet - name: DotNet Build diff --git a/ci-build.ps1 b/ci-build.ps1 index d45af6ff4d..74d4328e32 100644 --- a/ci-build.ps1 +++ b/ci-build.ps1 @@ -3,9 +3,16 @@ param( [string]$targetFramework ) +#$env:DOTNET_ROOT = "/usr/share/dotnet" +#$env:PATH = "$env:DOTNET_ROOT" + [System.IO.Path]::PathSeparator + $env:PATH + +# Confirm dotnet version. +dotnet --version + dotnet clean -c Release $repositoryUrl = "https://github.com/$env:GITHUB_REPOSITORY" + # Building for a specific framework. dotnet build -c Release -f $targetFramework /p:RepositoryUrl=$repositoryUrl From 069ca5a11755ea5f3cc56f86fb8c98b01afd1d92 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 30 Jan 2025 16:01:44 +1000 Subject: [PATCH 08/18] Add more debugging info --- .github/workflows/build-and-test.yml | 3 ++- ci-build.ps1 | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 1edb5ae81e..83bab0d837 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -122,10 +122,11 @@ jobs: 7.0.x - name: Ensure DotNet Path + if: ${{ matrix.options.os != 'windows-latest' }} run: echo "/usr/share/dotnet" >> $GITHUB_PATH - name: Confirm DotNet Version - run: dotnet --version # This will now find dotnet in /usr/share/dotnet + run: dotnet --version - name: DotNet Build if: ${{ matrix.options.sdk-preview != true }} diff --git a/ci-build.ps1 b/ci-build.ps1 index 74d4328e32..a98a5e27f8 100644 --- a/ci-build.ps1 +++ b/ci-build.ps1 @@ -3,8 +3,9 @@ param( [string]$targetFramework ) -#$env:DOTNET_ROOT = "/usr/share/dotnet" -#$env:PATH = "$env:DOTNET_ROOT" + [System.IO.Path]::PathSeparator + $env:PATH +Write-Output $env:PATH + +dotnet --list-sdks # Confirm dotnet version. dotnet --version @@ -13,6 +14,5 @@ dotnet clean -c Release $repositoryUrl = "https://github.com/$env:GITHUB_REPOSITORY" - # Building for a specific framework. dotnet build -c Release -f $targetFramework /p:RepositoryUrl=$repositoryUrl From 12b8e0dc428e8a86ffaa4c7eb1df4b4795dc089e Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 30 Jan 2025 16:06:55 +1000 Subject: [PATCH 09/18] Test windows only --- .github/workflows/build-and-test.yml | 68 ++++++++++++++-------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 83bab0d837..ccab6d574b 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -19,40 +19,40 @@ jobs: isARM: - ${{ contains(github.event.pull_request.labels.*.name, 'arch:arm32') || contains(github.event.pull_request.labels.*.name, 'arch:arm64') }} options: - - os: ubuntu-latest - framework: net7.0 - sdk: 7.0.x - sdk-preview: true - runtime: -x64 - codecov: false - - os: macos-13 # macos-latest runs on arm64 runners where libgdiplus is unavailable - framework: net7.0 - sdk: 7.0.x - sdk-preview: true - runtime: -x64 - codecov: false - - os: windows-latest - framework: net7.0 - sdk: 7.0.x - sdk-preview: true - runtime: -x64 - codecov: false - - os: buildjet-4vcpu-ubuntu-2204-arm - framework: net7.0 - sdk: 7.0.x - sdk-preview: true - runtime: -x64 - codecov: false - - os: ubuntu-latest - framework: net6.0 - sdk: 6.0.x - runtime: -x64 - codecov: false - - os: macos-13 # macos-latest runs on arm64 runners where libgdiplus is unavailable - framework: net6.0 - sdk: 6.0.x - runtime: -x64 - codecov: false + # - os: ubuntu-latest + # framework: net7.0 + # sdk: 7.0.x + # sdk-preview: true + # runtime: -x64 + # codecov: false + # - os: macos-13 # macos-latest runs on arm64 runners where libgdiplus is unavailable + # framework: net7.0 + # sdk: 7.0.x + # sdk-preview: true + # runtime: -x64 + # codecov: false + # - os: windows-latest + # framework: net7.0 + # sdk: 7.0.x + # sdk-preview: true + # runtime: -x64 + # codecov: false + # - os: buildjet-4vcpu-ubuntu-2204-arm + # framework: net7.0 + # sdk: 7.0.x + # sdk-preview: true + # runtime: -x64 + # codecov: false + # - os: ubuntu-latest + # framework: net6.0 + # sdk: 6.0.x + # runtime: -x64 + # codecov: false + # - os: macos-13 # macos-latest runs on arm64 runners where libgdiplus is unavailable + # framework: net6.0 + # sdk: 6.0.x + # runtime: -x64 + # codecov: false - os: windows-latest framework: net6.0 sdk: 6.0.x From e046be656dd2010b254ba9fc4d5bdb6dd097bdfa Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 30 Jan 2025 16:24:19 +1000 Subject: [PATCH 10/18] Now test ubuntu net6 again --- .github/workflows/build-and-test.yml | 14 +++++++------- ci-build.ps1 | 3 +++ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index ccab6d574b..a0f9f1b8bc 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -43,21 +43,21 @@ jobs: # sdk-preview: true # runtime: -x64 # codecov: false - # - os: ubuntu-latest + - os: ubuntu-latest + framework: net6.0 + sdk: 6.0.x + runtime: -x64 + codecov: false + # - os: macos-13 # macos-latest runs on arm64 runners where libgdiplus is unavailable # framework: net6.0 # sdk: 6.0.x # runtime: -x64 # codecov: false - # - os: macos-13 # macos-latest runs on arm64 runners where libgdiplus is unavailable + # - os: windows-latest # framework: net6.0 # sdk: 6.0.x # runtime: -x64 # codecov: false - - os: windows-latest - framework: net6.0 - sdk: 6.0.x - runtime: -x64 - codecov: false exclude: - isARM: false options: diff --git a/ci-build.ps1 b/ci-build.ps1 index a98a5e27f8..d465d8da79 100644 --- a/ci-build.ps1 +++ b/ci-build.ps1 @@ -3,11 +3,14 @@ param( [string]$targetFramework ) +Write-Output "PATH" Write-Output $env:PATH +Write-Output "DOTNET_LIST" dotnet --list-sdks # Confirm dotnet version. +Write-Output "DOTNET_VERSION" dotnet --version dotnet clean -c Release From 3a286c53bb7e8bbde0cd90c943b3d7b84949b120 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 30 Jan 2025 16:27:50 +1000 Subject: [PATCH 11/18] Try installing multiple sdks --- .github/workflows/build-and-test.yml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index a0f9f1b8bc..b46429379d 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -112,6 +112,8 @@ jobs: uses: actions/setup-dotnet@v4 with: dotnet-version: | + 8.0.x + 7.0.x 6.0.x - name: DotNet Setup Preview @@ -121,12 +123,12 @@ jobs: dotnet-version: | 7.0.x - - name: Ensure DotNet Path - if: ${{ matrix.options.os != 'windows-latest' }} - run: echo "/usr/share/dotnet" >> $GITHUB_PATH + # - name: Ensure DotNet Path + # if: ${{ matrix.options.os != 'windows-latest' }} + # run: echo "/usr/share/dotnet" >> $GITHUB_PATH - - name: Confirm DotNet Version - run: dotnet --version + # - name: Confirm DotNet Version + # run: dotnet --version - name: DotNet Build if: ${{ matrix.options.sdk-preview != true }} From 49a11182efd96a1b911c9a94414c4c42277058b0 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 30 Jan 2025 16:36:28 +1000 Subject: [PATCH 12/18] Re-enable all builds --- .github/workflows/build-and-test.yml | 83 ++++++++++++---------------- .github/workflows/code-coverage.yml | 8 +-- ci-build.ps1 | 10 ---- 3 files changed, 36 insertions(+), 65 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index b46429379d..a35175ff49 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -19,45 +19,45 @@ jobs: isARM: - ${{ contains(github.event.pull_request.labels.*.name, 'arch:arm32') || contains(github.event.pull_request.labels.*.name, 'arch:arm64') }} options: - # - os: ubuntu-latest - # framework: net7.0 - # sdk: 7.0.x - # sdk-preview: true - # runtime: -x64 - # codecov: false - # - os: macos-13 # macos-latest runs on arm64 runners where libgdiplus is unavailable - # framework: net7.0 - # sdk: 7.0.x - # sdk-preview: true - # runtime: -x64 - # codecov: false - # - os: windows-latest - # framework: net7.0 - # sdk: 7.0.x - # sdk-preview: true - # runtime: -x64 - # codecov: false - # - os: buildjet-4vcpu-ubuntu-2204-arm - # framework: net7.0 - # sdk: 7.0.x - # sdk-preview: true - # runtime: -x64 - # codecov: false - os: ubuntu-latest + framework: net7.0 + sdk: 7.0.x + sdk-preview: true + runtime: -x64 + codecov: false + - os: macos-13 # macos-latest runs on arm64 runners where libgdiplus is unavailable + framework: net7.0 + sdk: 7.0.x + sdk-preview: true + runtime: -x64 + codecov: false + - os: windows-latest + framework: net7.0 + sdk: 7.0.x + sdk-preview: true + runtime: -x64 + codecov: false + - os: buildjet-4vcpu-ubuntu-2204-arm + framework: net7.0 + sdk: 7.0.x + sdk-preview: true + runtime: -x64 + codecov: false + - os: ubuntu-latest + framework: net6.0 + sdk: 6.0.x + runtime: -x64 + codecov: false + - os: macos-13 # macos-latest runs on arm64 runners where libgdiplus is unavailable + framework: net6.0 + sdk: 6.0.x + runtime: -x64 + codecov: false + - os: windows-latest framework: net6.0 sdk: 6.0.x runtime: -x64 codecov: false - # - os: macos-13 # macos-latest runs on arm64 runners where libgdiplus is unavailable - # framework: net6.0 - # sdk: 6.0.x - # runtime: -x64 - # codecov: false - # - os: windows-latest - # framework: net6.0 - # sdk: 6.0.x - # runtime: -x64 - # codecov: false exclude: - isARM: false options: @@ -108,7 +108,6 @@ jobs: restore-keys: ${{ runner.os }}-nuget- - name: DotNet Setup - if: ${{ matrix.options.sdk-preview != true }} uses: actions/setup-dotnet@v4 with: dotnet-version: | @@ -116,20 +115,6 @@ jobs: 7.0.x 6.0.x - - name: DotNet Setup Preview - if: ${{ matrix.options.sdk-preview == true }} - uses: actions/setup-dotnet@v4 - with: - dotnet-version: | - 7.0.x - - # - name: Ensure DotNet Path - # if: ${{ matrix.options.os != 'windows-latest' }} - # run: echo "/usr/share/dotnet" >> $GITHUB_PATH - - # - name: Confirm DotNet Version - # run: dotnet --version - - name: DotNet Build if: ${{ matrix.options.sdk-preview != true }} shell: pwsh diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index da7d792cd8..db9aca0b08 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -61,14 +61,10 @@ jobs: uses: actions/setup-dotnet@v4 with: dotnet-version: | + 8.0.x + 7.0.x 6.0.x - - name: Ensure DotNet Path - run: echo "/usr/share/dotnet" >> $GITHUB_PATH - - - name: Confirm DotNet Version - run: dotnet --version # This will now find dotnet in /usr/share/dotnet - - name: DotNet Build shell: pwsh run: ./ci-build.ps1 "${{matrix.options.framework}}" diff --git a/ci-build.ps1 b/ci-build.ps1 index d465d8da79..d45af6ff4d 100644 --- a/ci-build.ps1 +++ b/ci-build.ps1 @@ -3,16 +3,6 @@ param( [string]$targetFramework ) -Write-Output "PATH" -Write-Output $env:PATH - -Write-Output "DOTNET_LIST" -dotnet --list-sdks - -# Confirm dotnet version. -Write-Output "DOTNET_VERSION" -dotnet --version - dotnet clean -c Release $repositoryUrl = "https://github.com/$env:GITHUB_REPOSITORY" From b99bf32bb19a1e88083cae48a8c96a7531859604 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 4 Feb 2025 18:41:10 +1000 Subject: [PATCH 13/18] Gracefully handle LZW overflows --- src/ImageSharp/Formats/Gif/LzwDecoder.cs | 6 ++++-- .../ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs | 12 ++++++++++++ tests/ImageSharp.Tests/TestImages.cs | 2 ++ tests/Images/Input/Gif/issues/issue_2859_A.gif | 3 +++ tests/Images/Input/Gif/issues/issue_2859_B.gif | 3 +++ 5 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 tests/Images/Input/Gif/issues/issue_2859_A.gif create mode 100644 tests/Images/Input/Gif/issues/issue_2859_B.gif diff --git a/src/ImageSharp/Formats/Gif/LzwDecoder.cs b/src/ImageSharp/Formats/Gif/LzwDecoder.cs index ec33f2b3ed..7a082e3674 100644 --- a/src/ImageSharp/Formats/Gif/LzwDecoder.cs +++ b/src/ImageSharp/Formats/Gif/LzwDecoder.cs @@ -137,6 +137,7 @@ public void DecodePixelRow(Span indices) ref int suffixRef = ref MemoryMarshal.GetReference(this.suffix.GetSpan()); ref int pixelStackRef = ref MemoryMarshal.GetReference(this.pixelStack.GetSpan()); Span buffer = this.scratchBuffer.GetSpan(); + int maxTop = this.pixelStack.Length() - 1; int x = 0; int xyz = 0; @@ -204,7 +205,7 @@ public void DecodePixelRow(Span indices) this.code = this.oldCode; } - while (this.code > this.clearCode) + while (this.code > this.clearCode && this.top < maxTop) { Unsafe.Add(ref pixelStackRef, (uint)this.top++) = Unsafe.Add(ref suffixRef, (uint)this.code); this.code = Unsafe.Add(ref prefixRef, (uint)this.code); @@ -250,6 +251,7 @@ public void SkipIndices(int length) ref int suffixRef = ref MemoryMarshal.GetReference(this.suffix.GetSpan()); ref int pixelStackRef = ref MemoryMarshal.GetReference(this.pixelStack.GetSpan()); Span buffer = this.scratchBuffer.GetSpan(); + int maxTop = this.pixelStack.Length() - 1; int xyz = 0; while (xyz < length) @@ -316,7 +318,7 @@ public void SkipIndices(int length) this.code = this.oldCode; } - while (this.code > this.clearCode) + while (this.code > this.clearCode && this.top < maxTop) { Unsafe.Add(ref pixelStackRef, (uint)this.top++) = Unsafe.Add(ref suffixRef, (uint)this.code); this.code = Unsafe.Add(ref prefixRef, (uint)this.code); diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs index f4e6487a57..bc6eeedcbe 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs @@ -334,4 +334,16 @@ public void IssueTooLargeLzwBits(TestImageProvider provider) image.DebugSaveMultiFrame(provider); image.CompareToReferenceOutputMultiFrame(provider, ImageComparer.Exact); } + + // https://github.com/SixLabors/ImageSharp/issues/2859 + [Theory] + [WithFile(TestImages.Gif.Issues.Issue2859_A, PixelTypes.Rgba32)] + [WithFile(TestImages.Gif.Issues.Issue2859_B, PixelTypes.Rgba32)] + public void Issue2859_LZWPixelStackOverflow(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + image.DebugSaveMultiFrame(provider); + image.CompareToReferenceOutputMultiFrame(provider, ImageComparer.Exact); + } } diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index a0e951e70d..fa3f38799a 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -535,6 +535,8 @@ public static class Issues public const string Issue2450_B = "Gif/issues/issue_2450_2.gif"; public const string Issue2198 = "Gif/issues/issue_2198.gif"; public const string Issue2758 = "Gif/issues/issue_2758.gif"; + public const string Issue2859_A = "Gif/issues/issue_2859_A.gif"; + public const string Issue2859_B = "Gif/issues/issue_2859_B.gif"; } public static readonly string[] Animated = diff --git a/tests/Images/Input/Gif/issues/issue_2859_A.gif b/tests/Images/Input/Gif/issues/issue_2859_A.gif new file mode 100644 index 0000000000..f19a047525 --- /dev/null +++ b/tests/Images/Input/Gif/issues/issue_2859_A.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:50a1a4afc62a3a36ff83596f1eb55d91cdd184c64e0d1339bbea17205c23eee1 +size 3406142 diff --git a/tests/Images/Input/Gif/issues/issue_2859_B.gif b/tests/Images/Input/Gif/issues/issue_2859_B.gif new file mode 100644 index 0000000000..109b0f8797 --- /dev/null +++ b/tests/Images/Input/Gif/issues/issue_2859_B.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:db9b2992be772a4f0ac495e994a17c7c50fb6de9794cfb9afc4a3dc26ffdfae0 +size 4543 From 170979743ece9b10414868865897378be8eb7ba8 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 4 Feb 2025 18:44:25 +1000 Subject: [PATCH 14/18] Add reference output --- .../00.png | 3 +++ .../00.png | 3 +++ .../01.png | 3 +++ 3 files changed, 9 insertions(+) create mode 100644 tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2859_LZWPixelStackOverflow_Rgba32_issue_2859_A.gif/00.png create mode 100644 tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2859_LZWPixelStackOverflow_Rgba32_issue_2859_B.gif/00.png create mode 100644 tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2859_LZWPixelStackOverflow_Rgba32_issue_2859_B.gif/01.png diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2859_LZWPixelStackOverflow_Rgba32_issue_2859_A.gif/00.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2859_LZWPixelStackOverflow_Rgba32_issue_2859_A.gif/00.png new file mode 100644 index 0000000000..d8c8df1263 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2859_LZWPixelStackOverflow_Rgba32_issue_2859_A.gif/00.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:daa78347749c6ff49891e2e379a373599cd35c98b453af9bf8eac52f615f935c +size 12237 diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2859_LZWPixelStackOverflow_Rgba32_issue_2859_B.gif/00.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2859_LZWPixelStackOverflow_Rgba32_issue_2859_B.gif/00.png new file mode 100644 index 0000000000..36c3683187 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2859_LZWPixelStackOverflow_Rgba32_issue_2859_B.gif/00.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:731299281f942f277ce6803e0adda3b5dd0395eb79cae26cabc9d56905fae0fd +size 1833 diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2859_LZWPixelStackOverflow_Rgba32_issue_2859_B.gif/01.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2859_LZWPixelStackOverflow_Rgba32_issue_2859_B.gif/01.png new file mode 100644 index 0000000000..c03e5817f0 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2859_LZWPixelStackOverflow_Rgba32_issue_2859_B.gif/01.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:50ccac7739142578d99a76b6d39ba377099d4a7ac30cbb0a5aee44ef1e7c9c8c +size 1271 From 9cb022bc96882c0484d6f617babb5141e4d224f0 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 4 Feb 2025 18:52:00 +1000 Subject: [PATCH 15/18] Update build-and-test.yml --- .github/workflows/build-and-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index a35175ff49..d9e0f1e08f 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -146,7 +146,7 @@ jobs: XUNIT_PATH: .\tests\ImageSharp.Tests # Required for xunit - name: Export Failed Output - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: failure() with: name: actual_output_${{ runner.os }}_${{ matrix.options.framework }}${{ matrix.options.runtime }}.zip From ca077545c45263f17e5f7764a155cf20e44f5580 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 5 Feb 2025 15:15:01 +1000 Subject: [PATCH 16/18] Use safe code. --- src/ImageSharp/Formats/Gif/LzwDecoder.cs | 308 ++++++++++++++--------- 1 file changed, 183 insertions(+), 125 deletions(-) diff --git a/src/ImageSharp/Formats/Gif/LzwDecoder.cs b/src/ImageSharp/Formats/Gif/LzwDecoder.cs index 7a082e3674..ece0f22888 100644 --- a/src/ImageSharp/Formats/Gif/LzwDecoder.cs +++ b/src/ImageSharp/Formats/Gif/LzwDecoder.cs @@ -3,7 +3,6 @@ using System.Buffers; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; @@ -37,22 +36,22 @@ internal sealed class LzwDecoder : IDisposable /// /// The prefix buffer. /// - private readonly IMemoryOwner prefix; + private readonly IMemoryOwner prefixOwner; /// /// The suffix buffer. /// - private readonly IMemoryOwner suffix; + private readonly IMemoryOwner suffixOwner; /// /// The scratch buffer for reading data blocks. /// - private readonly IMemoryOwner scratchBuffer; + private readonly IMemoryOwner bufferOwner; /// /// The pixel stack buffer. /// - private readonly IMemoryOwner pixelStack; + private readonly IMemoryOwner pixelStackOwner; private readonly int minCodeSize; private readonly int clearCode; private readonly int endCode; @@ -80,10 +79,10 @@ public LzwDecoder(MemoryAllocator memoryAllocator, BufferedReadStream stream, in { this.stream = stream ?? throw new ArgumentNullException(nameof(stream)); - this.prefix = memoryAllocator.Allocate(MaxStackSize, AllocationOptions.Clean); - this.suffix = memoryAllocator.Allocate(MaxStackSize, AllocationOptions.Clean); - this.pixelStack = memoryAllocator.Allocate(MaxStackSize + 1, AllocationOptions.Clean); - this.scratchBuffer = memoryAllocator.Allocate(byte.MaxValue, AllocationOptions.None); + this.prefixOwner = memoryAllocator.Allocate(MaxStackSize, AllocationOptions.Clean); + this.suffixOwner = memoryAllocator.Allocate(MaxStackSize, AllocationOptions.Clean); + this.pixelStackOwner = memoryAllocator.Allocate(MaxStackSize + 1, AllocationOptions.Clean); + this.bufferOwner = memoryAllocator.Allocate(byte.MaxValue, AllocationOptions.None); this.minCodeSize = minCodeSize; // Calculate the clear code. The value of the clear code is 2 ^ minCodeSize @@ -93,10 +92,11 @@ public LzwDecoder(MemoryAllocator memoryAllocator, BufferedReadStream stream, in this.endCode = this.clearCode + 1; this.availableCode = this.clearCode + 2; - ref int suffixRef = ref MemoryMarshal.GetReference(this.suffix.GetSpan()); - for (this.code = 0; this.code < this.clearCode; this.code++) + Span suffix = this.suffixOwner.GetSpan(); + int max = Math.Min(this.clearCode, suffix.Length); + for (this.code = 0; this.code < max; this.code++) { - Unsafe.Add(ref suffixRef, (uint)this.code) = (byte)this.code; + suffix[this.code] = (byte)this.code; } } @@ -132,113 +132,140 @@ public void DecodePixelRow(Span indices) { indices.Clear(); - ref byte pixelsRowRef = ref MemoryMarshal.GetReference(indices); - ref int prefixRef = ref MemoryMarshal.GetReference(this.prefix.GetSpan()); - ref int suffixRef = ref MemoryMarshal.GetReference(this.suffix.GetSpan()); - ref int pixelStackRef = ref MemoryMarshal.GetReference(this.pixelStack.GetSpan()); - Span buffer = this.scratchBuffer.GetSpan(); - int maxTop = this.pixelStack.Length() - 1; + // Get span values from the owners. + Span prefix = this.prefixOwner.GetSpan(); + Span suffix = this.suffixOwner.GetSpan(); + Span pixelStack = this.pixelStackOwner.GetSpan(); + Span buffer = this.bufferOwner.GetSpan(); + int maxTop = this.pixelStackOwner.Length() - 1; + + // Cache frequently accessed instance fields into locals. + // This helps avoid repeated field loads inside the tight loop. + BufferedReadStream stream = this.stream; + int top = this.top; + int bits = this.bits; + int codeSize = this.codeSize; + int codeMask = this.codeMask; + int minCodeSize = this.minCodeSize; + int availableCode = this.availableCode; + int oldCode = this.oldCode; + int first = this.first; + int data = this.data; + int count = this.count; + int bufferIndex = this.bufferIndex; + int code = this.code; + int clearCode = this.clearCode; + int endCode = this.endCode; - int x = 0; int xyz = 0; while (xyz < indices.Length) { - if (this.top == 0) + if (top == 0) { - if (this.bits < this.codeSize) + if (bits < codeSize) { // Load bytes until there are enough bits for a code. - if (this.count == 0) + if (count == 0) { // Read a new data block. - this.count = this.ReadBlock(buffer); - if (this.count == 0) + count = ReadBlock(stream, buffer); + if (count == 0) { break; } - this.bufferIndex = 0; + bufferIndex = 0; } - this.data += buffer[this.bufferIndex] << this.bits; - - this.bits += 8; - this.bufferIndex++; - this.count--; + data += buffer[bufferIndex] << bits; + bits += 8; + bufferIndex++; + count--; continue; } // Get the next code - this.code = this.data & this.codeMask; - this.data >>= this.codeSize; - this.bits -= this.codeSize; + code = data & codeMask; + data >>= codeSize; + bits -= codeSize; // Interpret the code - if (this.code > this.availableCode || this.code == this.endCode) + if (code > availableCode || code == endCode) { break; } - if (this.code == this.clearCode) + if (code == clearCode) { // Reset the decoder - this.codeSize = this.minCodeSize + 1; - this.codeMask = (1 << this.codeSize) - 1; - this.availableCode = this.clearCode + 2; - this.oldCode = NullCode; + codeSize = minCodeSize + 1; + codeMask = (1 << codeSize) - 1; + availableCode = clearCode + 2; + oldCode = NullCode; continue; } - if (this.oldCode == NullCode) + if (oldCode == NullCode) { - Unsafe.Add(ref pixelStackRef, (uint)this.top++) = Unsafe.Add(ref suffixRef, (uint)this.code); - this.oldCode = this.code; - this.first = this.code; + pixelStack[top++] = suffix[code]; + oldCode = code; + first = code; continue; } - int inCode = this.code; - if (this.code == this.availableCode) + int inCode = code; + if (code == availableCode) { - Unsafe.Add(ref pixelStackRef, (uint)this.top++) = (byte)this.first; - - this.code = this.oldCode; + pixelStack[top++] = first; + code = oldCode; } - while (this.code > this.clearCode && this.top < maxTop) + while (code > clearCode && top < maxTop) { - Unsafe.Add(ref pixelStackRef, (uint)this.top++) = Unsafe.Add(ref suffixRef, (uint)this.code); - this.code = Unsafe.Add(ref prefixRef, (uint)this.code); + pixelStack[top++] = suffix[code]; + code = prefix[code]; } - int suffixCode = Unsafe.Add(ref suffixRef, (uint)this.code); - this.first = suffixCode; - Unsafe.Add(ref pixelStackRef, (uint)this.top++) = suffixCode; + int suffixCode = suffix[code]; + first = suffixCode; + pixelStack[top++] = suffixCode; - // Fix for Gifs that have "deferred clear code" as per here : + // Fix for GIFs that have "deferred clear code" as per: // https://bugzilla.mozilla.org/show_bug.cgi?id=55918 - if (this.availableCode < MaxStackSize) + if (availableCode < MaxStackSize) { - Unsafe.Add(ref prefixRef, (uint)this.availableCode) = this.oldCode; - Unsafe.Add(ref suffixRef, (uint)this.availableCode) = this.first; - this.availableCode++; - if (this.availableCode == this.codeMask + 1 && this.availableCode < MaxStackSize) + prefix[availableCode] = oldCode; + suffix[availableCode] = first; + availableCode++; + if (availableCode == codeMask + 1 && availableCode < MaxStackSize) { - this.codeSize++; - this.codeMask = (1 << this.codeSize) - 1; + codeSize++; + codeMask = (1 << codeSize) - 1; } } - this.oldCode = inCode; + oldCode = inCode; } // Pop a pixel off the pixel stack. - this.top--; + top--; - // Clear missing pixels - xyz++; - Unsafe.Add(ref pixelsRowRef, (uint)x++) = (byte)Unsafe.Add(ref pixelStackRef, (uint)this.top); + // Clear missing pixels. + indices[xyz++] = (byte)pixelStack[top]; } + + // Write back the local values to the instance fields. + this.top = top; + this.bits = bits; + this.codeSize = codeSize; + this.codeMask = codeMask; + this.availableCode = availableCode; + this.oldCode = oldCode; + this.first = first; + this.data = data; + this.count = count; + this.bufferIndex = bufferIndex; + this.code = code; } /// @@ -247,131 +274,162 @@ public void DecodePixelRow(Span indices) /// The resulting index table length. public void SkipIndices(int length) { - ref int prefixRef = ref MemoryMarshal.GetReference(this.prefix.GetSpan()); - ref int suffixRef = ref MemoryMarshal.GetReference(this.suffix.GetSpan()); - ref int pixelStackRef = ref MemoryMarshal.GetReference(this.pixelStack.GetSpan()); - Span buffer = this.scratchBuffer.GetSpan(); - int maxTop = this.pixelStack.Length() - 1; + // Get span values from the owners. + Span prefix = this.prefixOwner.GetSpan(); + Span suffix = this.suffixOwner.GetSpan(); + Span pixelStack = this.pixelStackOwner.GetSpan(); + Span buffer = this.bufferOwner.GetSpan(); + int maxTop = this.pixelStackOwner.Length() - 1; + + // Cache frequently accessed instance fields into locals. + // This helps avoid repeated field loads inside the tight loop. + BufferedReadStream stream = this.stream; + int top = this.top; + int bits = this.bits; + int codeSize = this.codeSize; + int codeMask = this.codeMask; + int minCodeSize = this.minCodeSize; + int availableCode = this.availableCode; + int oldCode = this.oldCode; + int first = this.first; + int data = this.data; + int count = this.count; + int bufferIndex = this.bufferIndex; + int code = this.code; + int clearCode = this.clearCode; + int endCode = this.endCode; int xyz = 0; while (xyz < length) { - if (this.top == 0) + if (top == 0) { - if (this.bits < this.codeSize) + if (bits < codeSize) { // Load bytes until there are enough bits for a code. - if (this.count == 0) + if (count == 0) { // Read a new data block. - this.count = this.ReadBlock(buffer); - if (this.count == 0) + count = ReadBlock(stream, buffer); + if (count == 0) { break; } - this.bufferIndex = 0; + bufferIndex = 0; } - this.data += buffer[this.bufferIndex] << this.bits; - - this.bits += 8; - this.bufferIndex++; - this.count--; + data += buffer[bufferIndex] << bits; + bits += 8; + bufferIndex++; + count--; continue; } // Get the next code - this.code = this.data & this.codeMask; - this.data >>= this.codeSize; - this.bits -= this.codeSize; + code = data & codeMask; + data >>= codeSize; + bits -= codeSize; // Interpret the code - if (this.code > this.availableCode || this.code == this.endCode) + if (code > availableCode || code == endCode) { break; } - if (this.code == this.clearCode) + if (code == clearCode) { // Reset the decoder - this.codeSize = this.minCodeSize + 1; - this.codeMask = (1 << this.codeSize) - 1; - this.availableCode = this.clearCode + 2; - this.oldCode = NullCode; + codeSize = minCodeSize + 1; + codeMask = (1 << codeSize) - 1; + availableCode = clearCode + 2; + oldCode = NullCode; continue; } - if (this.oldCode == NullCode) + if (oldCode == NullCode) { - Unsafe.Add(ref pixelStackRef, (uint)this.top++) = Unsafe.Add(ref suffixRef, (uint)this.code); - this.oldCode = this.code; - this.first = this.code; + pixelStack[top++] = suffix[code]; + oldCode = code; + first = code; continue; } - int inCode = this.code; - if (this.code == this.availableCode) + int inCode = code; + if (code == availableCode) { - Unsafe.Add(ref pixelStackRef, (uint)this.top++) = (byte)this.first; - - this.code = this.oldCode; + pixelStack[top++] = first; + code = oldCode; } - while (this.code > this.clearCode && this.top < maxTop) + while (code > clearCode && top < maxTop) { - Unsafe.Add(ref pixelStackRef, (uint)this.top++) = Unsafe.Add(ref suffixRef, (uint)this.code); - this.code = Unsafe.Add(ref prefixRef, (uint)this.code); + pixelStack[top++] = suffix[code]; + code = prefix[code]; } - int suffixCode = Unsafe.Add(ref suffixRef, (uint)this.code); - this.first = suffixCode; - Unsafe.Add(ref pixelStackRef, (uint)this.top++) = suffixCode; + int suffixCode = suffix[code]; + first = suffixCode; + pixelStack[top++] = suffixCode; - // Fix for Gifs that have "deferred clear code" as per here : + // Fix for GIFs that have "deferred clear code" as per: // https://bugzilla.mozilla.org/show_bug.cgi?id=55918 - if (this.availableCode < MaxStackSize) + if (availableCode < MaxStackSize) { - Unsafe.Add(ref prefixRef, (uint)this.availableCode) = this.oldCode; - Unsafe.Add(ref suffixRef, (uint)this.availableCode) = this.first; - this.availableCode++; - if (this.availableCode == this.codeMask + 1 && this.availableCode < MaxStackSize) + prefix[availableCode] = oldCode; + suffix[availableCode] = first; + availableCode++; + if (availableCode == codeMask + 1 && availableCode < MaxStackSize) { - this.codeSize++; - this.codeMask = (1 << this.codeSize) - 1; + codeSize++; + codeMask = (1 << codeSize) - 1; } } - this.oldCode = inCode; + oldCode = inCode; } // Pop a pixel off the pixel stack. - this.top--; + top--; - // Clear missing pixels + // Skip missing pixels. xyz++; } + + // Write back the local values to the instance fields. + this.top = top; + this.bits = bits; + this.codeSize = codeSize; + this.codeMask = codeMask; + this.availableCode = availableCode; + this.oldCode = oldCode; + this.first = first; + this.data = data; + this.count = count; + this.bufferIndex = bufferIndex; + this.code = code; } /// /// Reads the next data block from the stream. A data block begins with a byte, /// which defines the size of the block, followed by the block itself. /// + /// The stream to read from. /// The buffer to store the block in. /// /// The . /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - private int ReadBlock(Span buffer) + private static int ReadBlock(BufferedReadStream stream, Span buffer) { - int bufferSize = this.stream.ReadByte(); + int bufferSize = stream.ReadByte(); if (bufferSize < 1) { return 0; } - int count = this.stream.Read(buffer, 0, bufferSize); + int count = stream.Read(buffer, 0, bufferSize); return count != bufferSize ? 0 : bufferSize; } @@ -379,9 +437,9 @@ private int ReadBlock(Span buffer) /// public void Dispose() { - this.prefix.Dispose(); - this.suffix.Dispose(); - this.pixelStack.Dispose(); - this.scratchBuffer.Dispose(); + this.prefixOwner.Dispose(); + this.suffixOwner.Dispose(); + this.pixelStackOwner.Dispose(); + this.bufferOwner.Dispose(); } } From 20e5596f2a450c7911053cf3ba3fb7d5f03f7f62 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 10 Feb 2025 11:45:47 +1000 Subject: [PATCH 17/18] Clean up and optimize --- src/ImageSharp/Formats/Gif/LzwDecoder.cs | 33 ++++++++++++------------ 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/src/ImageSharp/Formats/Gif/LzwDecoder.cs b/src/ImageSharp/Formats/Gif/LzwDecoder.cs index ece0f22888..b33d1f3d48 100644 --- a/src/ImageSharp/Formats/Gif/LzwDecoder.cs +++ b/src/ImageSharp/Formats/Gif/LzwDecoder.cs @@ -78,6 +78,7 @@ internal sealed class LzwDecoder : IDisposable public LzwDecoder(MemoryAllocator memoryAllocator, BufferedReadStream stream, int minCodeSize) { this.stream = stream ?? throw new ArgumentNullException(nameof(stream)); + Guard.IsTrue(IsValidMinCodeSize(minCodeSize), nameof(minCodeSize), "Invalid minimum code size."); this.prefixOwner = memoryAllocator.Allocate(MaxStackSize, AllocationOptions.Clean); this.suffixOwner = memoryAllocator.Allocate(MaxStackSize, AllocationOptions.Clean); @@ -92,12 +93,15 @@ public LzwDecoder(MemoryAllocator memoryAllocator, BufferedReadStream stream, in this.endCode = this.clearCode + 1; this.availableCode = this.clearCode + 2; - Span suffix = this.suffixOwner.GetSpan(); - int max = Math.Min(this.clearCode, suffix.Length); - for (this.code = 0; this.code < max; this.code++) + // Fill the suffix buffer with the initial values represented by the number of colors. + Span suffix = this.suffixOwner.GetSpan()[..this.clearCode]; + int i; + for (i = 0; i < suffix.Length; i++) { - suffix[this.code] = (byte)this.code; + suffix[i] = i; } + + this.code = i; } /// @@ -112,8 +116,7 @@ public static bool IsValidMinCodeSize(int minCodeSize) // It is possible to specify a larger LZW minimum code size than the palette length in bits // which may leave a gap in the codes where no colors are assigned. // http://www.matthewflickinger.com/lab/whatsinagif/lzw_image_data.asp#lzw_compression - int clearCode = 1 << minCodeSize; - if (minCodeSize < 2 || minCodeSize > MaximumLzwBits || clearCode > MaxStackSize) + if (minCodeSize < 2 || minCodeSize > MaximumLzwBits || 1 << minCodeSize > MaxStackSize) { // Don't attempt to decode the frame indices. // Theoretically we could determine a min code size from the length of the provided @@ -137,7 +140,6 @@ public void DecodePixelRow(Span indices) Span suffix = this.suffixOwner.GetSpan(); Span pixelStack = this.pixelStackOwner.GetSpan(); Span buffer = this.bufferOwner.GetSpan(); - int maxTop = this.pixelStackOwner.Length() - 1; // Cache frequently accessed instance fields into locals. // This helps avoid repeated field loads inside the tight loop. @@ -157,8 +159,8 @@ public void DecodePixelRow(Span indices) int clearCode = this.clearCode; int endCode = this.endCode; - int xyz = 0; - while (xyz < indices.Length) + int i = 0; + while (i < indices.Length) { if (top == 0) { @@ -220,7 +222,7 @@ public void DecodePixelRow(Span indices) code = oldCode; } - while (code > clearCode && top < maxTop) + while (code > clearCode && top < MaxStackSize) { pixelStack[top++] = suffix[code]; code = prefix[code]; @@ -251,7 +253,7 @@ public void DecodePixelRow(Span indices) top--; // Clear missing pixels. - indices[xyz++] = (byte)pixelStack[top]; + indices[i++] = (byte)pixelStack[top]; } // Write back the local values to the instance fields. @@ -279,7 +281,6 @@ public void SkipIndices(int length) Span suffix = this.suffixOwner.GetSpan(); Span pixelStack = this.pixelStackOwner.GetSpan(); Span buffer = this.bufferOwner.GetSpan(); - int maxTop = this.pixelStackOwner.Length() - 1; // Cache frequently accessed instance fields into locals. // This helps avoid repeated field loads inside the tight loop. @@ -299,8 +300,8 @@ public void SkipIndices(int length) int clearCode = this.clearCode; int endCode = this.endCode; - int xyz = 0; - while (xyz < length) + int i = 0; + while (i < length) { if (top == 0) { @@ -362,7 +363,7 @@ public void SkipIndices(int length) code = oldCode; } - while (code > clearCode && top < maxTop) + while (code > clearCode && top < MaxStackSize) { pixelStack[top++] = suffix[code]; code = prefix[code]; @@ -393,7 +394,7 @@ public void SkipIndices(int length) top--; // Skip missing pixels. - xyz++; + i++; } // Write back the local values to the instance fields. From d04ec7b76635ee94521a232421ce96f4abab3969 Mon Sep 17 00:00:00 2001 From: antonfirsov Date: Mon, 3 Mar 2025 17:38:59 +0100 Subject: [PATCH 18/18] workaround build issues --- src/ImageSharp/ImageSharp.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ImageSharp/ImageSharp.csproj b/src/ImageSharp/ImageSharp.csproj index b08c27c41b..be29b40a0e 100644 --- a/src/ImageSharp/ImageSharp.csproj +++ b/src/ImageSharp/ImageSharp.csproj @@ -13,6 +13,7 @@ Image Resize Crop Gif Jpg Jpeg Bitmap Pbm Png Tga Tiff WebP NetCore A new, fully featured, fully managed, cross-platform, 2D graphics API for .NET Debug;Release + preview