Browse Source

Merge branch 'master' into bp/alphadecodingsse

pull/1930/head
James Jackson-South 4 years ago
committed by GitHub
parent
commit
2e659faa50
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 390
      .github/workflows/build-and-test.yml
  2. 81
      .github/workflows/code-coverage.yml
  3. 1
      ImageSharp.sln
  4. 11
      src/ImageSharp/Formats/Gif/GifConstants.cs
  5. 39
      src/ImageSharp/Formats/Gif/GifDecoderCore.cs
  6. 47
      src/ImageSharp/Formats/Gif/GifEncoderCore.cs
  7. 4
      src/ImageSharp/Formats/Gif/Sections/GifGraphicControlExtension.cs
  8. 2
      src/ImageSharp/Formats/Gif/Sections/GifNetscapeLoopingApplicationExtension.cs
  9. 97
      src/ImageSharp/Formats/Gif/Sections/GifXmpApplicationExtension.cs
  10. 7
      src/ImageSharp/Formats/Gif/Sections/IGifExtension.cs
  11. 30
      src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs
  12. 76
      src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs
  13. 10
      src/ImageSharp/Formats/Jpeg/Components/Decoder/JFifMarker.cs
  14. 12
      src/ImageSharp/Formats/Jpeg/Components/Decoder/ProfileResolver.cs
  15. 166
      src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
  16. 57
      src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs
  17. 13
      src/ImageSharp/Formats/PixelTypeInfo.cs
  18. 24
      src/ImageSharp/Formats/Png/PngConstants.cs
  19. 19
      src/ImageSharp/Formats/Png/PngDecoderCore.cs
  20. 115
      src/ImageSharp/Formats/Png/PngEncoderCore.cs
  21. 9
      src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
  22. 7
      src/ImageSharp/Formats/Tiff/TiffDecoderMetadataCreator.cs
  23. 9
      src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs
  24. 37
      src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs
  25. 28
      src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs
  26. 24
      src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs
  27. 5
      src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs
  28. 6
      src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs
  29. 78
      src/ImageSharp/Formats/Webp/WebpDecoderCore.cs
  30. 1
      src/ImageSharp/IO/BufferedReadStream.cs
  31. 7
      src/ImageSharp/Metadata/ImageFrameMetadata.cs
  32. 10
      src/ImageSharp/Metadata/ImageMetadata.cs
  33. 31
      src/ImageSharp/Metadata/Profiles/Exif/ExifWriter.cs
  34. 89
      src/ImageSharp/Metadata/Profiles/XMP/XmpProfile.cs
  35. 9
      tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs
  36. 13
      tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs
  37. 20
      tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs
  38. 24
      tests/ImageSharp.Tests/IO/BufferedReadStreamTests.cs
  39. 8
      tests/ImageSharp.Tests/Metadata/ImageFrameMetadataTests.cs
  40. 31
      tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs
  41. 260
      tests/ImageSharp.Tests/Metadata/Profiles/XMP/XmpProfileTests.cs
  42. 5
      tests/ImageSharp.Tests/TestImages.cs
  43. 3
      tests/Images/Input/Jpg/baseline/Metadata-test-file.jpg
  44. 3
      tests/Images/Input/Jpg/baseline/extended-xmp.jpg
  45. 3
      tests/Images/Input/Jpg/issues/issue-1932-app0-resolution.jpg
  46. 3
      tests/Images/Input/Png/xmp-colorpalette.png
  47. 3
      tests/Images/Input/Webp/xmp_lossy.webp

390
.github/workflows/build-and-test.yml

@ -1,202 +1,196 @@
name: Build
on:
push:
branches:
- master
tags:
- "v*"
pull_request:
branches:
- master
push:
branches:
- master
tags:
- "v*"
pull_request:
branches:
- master
jobs:
Build:
strategy:
matrix:
options:
- os: ubuntu-latest
framework: net6.0
sdk: 6.0.x
sdk-preview: true
runtime: -x64
codecov: false
- os: macos-latest
framework: net6.0
sdk: 6.0.x
sdk-preview: true
runtime: -x64
codecov: false
- os: windows-latest
framework: net6.0
sdk: 6.0.x
sdk-preview: true
runtime: -x64
codecov: false
- os: ubuntu-latest
framework: net5.0
runtime: -x64
codecov: false
- os: macos-latest
framework: net5.0
runtime: -x64
codecov: false
- os: windows-latest
framework: net5.0
runtime: -x64
codecov: false
- os: ubuntu-latest
framework: netcoreapp3.1
runtime: -x64
codecov: true
- os: macos-latest
framework: netcoreapp3.1
runtime: -x64
codecov: false
- os: windows-latest
framework: netcoreapp3.1
runtime: -x64
codecov: false
- os: windows-latest
framework: netcoreapp2.1
runtime: -x64
codecov: false
- os: windows-latest
framework: net472
runtime: -x64
codecov: false
- os: windows-latest
framework: net472
runtime: -x86
codecov: false
runs-on: ${{matrix.options.os}}
steps:
- name: Git Config
shell: bash
run: |
git config --global core.autocrlf false
git config --global core.longpaths true
- name: Git Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
submodules: recursive
# See https://github.com/actions/checkout/issues/165#issuecomment-657673315
- name: Git Create LFS FileList
run: git lfs ls-files -l | cut -d' ' -f1 | sort > .lfs-assets-id
- name: Git Setup LFS Cache
uses: actions/cache@v2
id: lfs-cache
with:
path: .git/lfs
key: ${{ runner.os }}-lfs-${{ hashFiles('.lfs-assets-id') }}-v1
- name: Git Pull LFS
run: git lfs pull
- name: NuGet Install
uses: NuGet/setup-nuget@v1
- name: NuGet Setup Cache
uses: actions/cache@v2
id: nuget-cache
with:
path: ~/.nuget
key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', '**/*.props', '**/*.targets') }}
restore-keys: ${{ runner.os }}-nuget-
- name: DotNet Setup Preview
if: ${{ matrix.options.sdk-preview == true }}
uses: actions/setup-dotnet@v1
with:
dotnet-version: ${{ matrix.options.sdk }}
include-prerelease: true
- name: DotNet Build
if: ${{ matrix.options.sdk-preview != true }}
shell: pwsh
run: ./ci-build.ps1 "${{matrix.options.framework}}"
env:
SIXLABORS_TESTING: True
- name: DotNet Build Preview
if: ${{ matrix.options.sdk-preview == true }}
shell: pwsh
run: ./ci-build.ps1 "${{matrix.options.framework}}"
env:
SIXLABORS_TESTING_PREVIEW: True
- name: DotNet Test
if: ${{ matrix.options.sdk-preview != true }}
shell: pwsh
run: ./ci-test.ps1 "${{matrix.options.os}}" "${{matrix.options.framework}}" "${{matrix.options.runtime}}" "${{matrix.options.codecov}}"
env:
SIXLABORS_TESTING: True
XUNIT_PATH: .\tests\ImageSharp.Tests # Required for xunit
- name: DotNet Test Preview
if: ${{ matrix.options.sdk-preview == true }}
shell: pwsh
run: ./ci-test.ps1 "${{matrix.options.os}}" "${{matrix.options.framework}}" "${{matrix.options.runtime}}" "${{matrix.options.codecov}}"
env:
SIXLABORS_TESTING_PREVIEW: True
XUNIT_PATH: .\tests\ImageSharp.Tests # Required for xunit
- name: Export Failed Output
uses: actions/upload-artifact@v2
if: failure()
with:
name: actual_output_${{ runner.os }}_${{ matrix.options.framework }}${{ matrix.options.runtime }}.zip
path: tests/Images/ActualOutput/
- name: Codecov Update
uses: codecov/codecov-action@v1
if: matrix.options.codecov == true && startsWith(github.repository, 'SixLabors')
with:
flags: unittests
Publish:
needs: [Build]
runs-on: ubuntu-latest
if: (github.event_name == 'push')
steps:
- name: Git Config
shell: bash
run: |
git config --global core.autocrlf false
git config --global core.longpaths true
- name: Git Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
submodules: recursive
- name: NuGet Install
uses: NuGet/setup-nuget@v1
- name: NuGet Setup Cache
uses: actions/cache@v2
id: nuget-cache
with:
path: ~/.nuget
key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', '**/*.props', '**/*.targets') }}
restore-keys: ${{ runner.os }}-nuget-
- name: DotNet Pack
shell: pwsh
run: ./ci-pack.ps1
- name: MyGet Publish
shell: pwsh
run: |
dotnet nuget push .\artifacts\*.nupkg -k ${{secrets.MYGET_TOKEN}} -s https://www.myget.org/F/sixlabors/api/v2/package
dotnet nuget push .\artifacts\*.snupkg -k ${{secrets.MYGET_TOKEN}} -s https://www.myget.org/F/sixlabors/api/v3/index.json
# TODO: If github.ref starts with 'refs/tags' then it was tag push and we can optionally push out package to nuget.org
Build:
strategy:
matrix:
options:
- os: ubuntu-latest
framework: net6.0
sdk: 6.0.x
sdk-preview: true
runtime: -x64
codecov: false
- os: macos-latest
framework: net6.0
sdk: 6.0.x
sdk-preview: true
runtime: -x64
codecov: false
- os: windows-latest
framework: net6.0
sdk: 6.0.x
sdk-preview: true
runtime: -x64
codecov: false
- os: ubuntu-latest
framework: net5.0
runtime: -x64
codecov: false
- os: macos-latest
framework: net5.0
runtime: -x64
codecov: false
- os: windows-latest
framework: net5.0
runtime: -x64
codecov: false
- os: ubuntu-latest
framework: netcoreapp3.1
runtime: -x64
codecov: false
- os: macos-latest
framework: netcoreapp3.1
runtime: -x64
codecov: false
- os: windows-latest
framework: netcoreapp3.1
runtime: -x64
codecov: false
- os: windows-latest
framework: netcoreapp2.1
runtime: -x64
codecov: false
- os: windows-latest
framework: net472
runtime: -x64
codecov: false
- os: windows-latest
framework: net472
runtime: -x86
codecov: false
runs-on: ${{matrix.options.os}}
steps:
- name: Git Config
shell: bash
run: |
git config --global core.autocrlf false
git config --global core.longpaths true
- name: Git Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
submodules: recursive
# See https://github.com/actions/checkout/issues/165#issuecomment-657673315
- name: Git Create LFS FileList
run: git lfs ls-files -l | cut -d' ' -f1 | sort > .lfs-assets-id
- name: Git Setup LFS Cache
uses: actions/cache@v2
id: lfs-cache
with:
path: .git/lfs
key: ${{ runner.os }}-lfs-${{ hashFiles('.lfs-assets-id') }}-v1
- name: Git Pull LFS
run: git lfs pull
- name: NuGet Install
uses: NuGet/setup-nuget@v1
- name: NuGet Setup Cache
uses: actions/cache@v2
id: nuget-cache
with:
path: ~/.nuget
key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', '**/*.props', '**/*.targets') }}
restore-keys: ${{ runner.os }}-nuget-
- name: DotNet Setup Preview
if: ${{ matrix.options.sdk-preview == true }}
uses: actions/setup-dotnet@v1
with:
dotnet-version: ${{ matrix.options.sdk }}
include-prerelease: true
- name: DotNet Build
if: ${{ matrix.options.sdk-preview != true }}
shell: pwsh
run: ./ci-build.ps1 "${{matrix.options.framework}}"
env:
SIXLABORS_TESTING: True
- name: DotNet Build Preview
if: ${{ matrix.options.sdk-preview == true }}
shell: pwsh
run: ./ci-build.ps1 "${{matrix.options.framework}}"
env:
SIXLABORS_TESTING_PREVIEW: True
- name: DotNet Test
if: ${{ matrix.options.sdk-preview != true }}
shell: pwsh
run: ./ci-test.ps1 "${{matrix.options.os}}" "${{matrix.options.framework}}" "${{matrix.options.runtime}}" "${{matrix.options.codecov}}"
env:
SIXLABORS_TESTING: True
XUNIT_PATH: .\tests\ImageSharp.Tests # Required for xunit
- name: DotNet Test Preview
if: ${{ matrix.options.sdk-preview == true }}
shell: pwsh
run: ./ci-test.ps1 "${{matrix.options.os}}" "${{matrix.options.framework}}" "${{matrix.options.runtime}}" "${{matrix.options.codecov}}"
env:
SIXLABORS_TESTING_PREVIEW: True
XUNIT_PATH: .\tests\ImageSharp.Tests # Required for xunit
- name: Export Failed Output
uses: actions/upload-artifact@v2
if: failure()
with:
name: actual_output_${{ runner.os }}_${{ matrix.options.framework }}${{ matrix.options.runtime }}.zip
path: tests/Images/ActualOutput/
Publish:
needs: [Build]
runs-on: ubuntu-latest
if: (github.event_name == 'push')
steps:
- name: Git Config
shell: bash
run: |
git config --global core.autocrlf false
git config --global core.longpaths true
- name: Git Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
submodules: recursive
- name: NuGet Install
uses: NuGet/setup-nuget@v1
- name: NuGet Setup Cache
uses: actions/cache@v2
id: nuget-cache
with:
path: ~/.nuget
key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', '**/*.props', '**/*.targets') }}
restore-keys: ${{ runner.os }}-nuget-
- name: DotNet Pack
shell: pwsh
run: ./ci-pack.ps1
- name: MyGet Publish
shell: pwsh
run: |
dotnet nuget push .\artifacts\*.nupkg -k ${{secrets.MYGET_TOKEN}} -s https://www.myget.org/F/sixlabors/api/v2/package
dotnet nuget push .\artifacts\*.snupkg -k ${{secrets.MYGET_TOKEN}} -s https://www.myget.org/F/sixlabors/api/v3/index.json
# TODO: If github.ref starts with 'refs/tags' then it was tag push and we can optionally push out package to nuget.org

81
.github/workflows/code-coverage.yml

@ -0,0 +1,81 @@
name: CodeCoverage
on:
schedule:
# 2AM every Tuesday/Thursday
- cron: "0 2 * * 2,4"
jobs:
Build:
strategy:
matrix:
options:
- os: ubuntu-latest
framework: netcoreapp3.1
runtime: -x64
codecov: true
runs-on: ${{matrix.options.os}}
steps:
- name: Git Config
shell: bash
run: |
git config --global core.autocrlf false
git config --global core.longpaths true
- name: Git Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
submodules: recursive
# See https://github.com/actions/checkout/issues/165#issuecomment-657673315
- name: Git Create LFS FileList
run: git lfs ls-files -l | cut -d' ' -f1 | sort > .lfs-assets-id
- name: Git Setup LFS Cache
uses: actions/cache@v2
id: lfs-cache
with:
path: .git/lfs
key: ${{ runner.os }}-lfs-${{ hashFiles('.lfs-assets-id') }}-v1
- name: Git Pull LFS
run: git lfs pull
- name: NuGet Install
uses: NuGet/setup-nuget@v1
- name: NuGet Setup Cache
uses: actions/cache@v2
id: nuget-cache
with:
path: ~/.nuget
key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', '**/*.props', '**/*.targets') }}
restore-keys: ${{ runner.os }}-nuget-
- name: DotNet Build
shell: pwsh
run: ./ci-build.ps1 "${{matrix.options.framework}}"
env:
SIXLABORS_TESTING: True
- name: DotNet Test
shell: pwsh
run: ./ci-test.ps1 "${{matrix.options.os}}" "${{matrix.options.framework}}" "${{matrix.options.runtime}}" "${{matrix.options.codecov}}"
env:
SIXLABORS_TESTING: True
XUNIT_PATH: .\tests\ImageSharp.Tests # Required for xunit
- name: Export Failed Output
uses: actions/upload-artifact@v2
if: failure()
with:
name: actual_output_${{ runner.os }}_${{ matrix.options.framework }}${{ matrix.options.runtime }}.zip
path: tests/Images/ActualOutput/
- name: Codecov Update
uses: codecov/codecov-action@v1
if: matrix.options.codecov == true && startsWith(github.repository, 'SixLabors')
with:
flags: unittests

1
ImageSharp.sln

@ -551,6 +551,7 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{C0D7754B-5277-438E-ABEB-2BA34401B5A7}"
ProjectSection(SolutionItems) = preProject
.github\workflows\build-and-test.yml = .github\workflows\build-and-test.yml
.github\workflows\code-coverage.yml = .github\workflows\code-coverage.yml
EndProjectSection
EndProject
Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "SharedInfrastructure", "shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.shproj", "{68A8CC40-6AED-4E96-B524-31B1158FDEEA}"

11
src/ImageSharp/Formats/Gif/GifConstants.cs

@ -121,5 +121,16 @@ namespace SixLabors.ImageSharp.Formats.Gif
(byte)'P', (byte)'E',
(byte)'2', (byte)'.', (byte)'0'
};
/// <summary>
/// Gets the ASCII encoded application identification bytes.
/// </summary>
internal static ReadOnlySpan<byte> XmpApplicationIdentificationBytes => new[]
{
(byte)'X', (byte)'M', (byte)'P',
(byte)' ', (byte)'D', (byte)'a',
(byte)'t', (byte)'a',
(byte)'X', (byte)'M', (byte)'P'
};
}
}

39
src/ImageSharp/Formats/Gif/GifDecoderCore.cs

@ -11,6 +11,7 @@ using System.Threading;
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Gif
@ -250,7 +251,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
}
/// <summary>
/// Reads the application extension block parsing any animation information
/// Reads the application extension block parsing any animation or XMP information
/// if present.
/// </summary>
private void ReadApplicationExtension()
@ -258,25 +259,37 @@ namespace SixLabors.ImageSharp.Formats.Gif
int appLength = this.stream.ReadByte();
// If the length is 11 then it's a valid extension and most likely
// a NETSCAPE or ANIMEXTS extension. We want the loop count from this.
// a NETSCAPE, XMP or ANIMEXTS extension. We want the loop count from this.
if (appLength == GifConstants.ApplicationBlockSize)
{
this.stream.Skip(appLength);
int subBlockSize = this.stream.ReadByte();
this.stream.Read(this.buffer, 0, GifConstants.ApplicationBlockSize);
bool isXmp = this.buffer.AsSpan().StartsWith(GifConstants.XmpApplicationIdentificationBytes);
// TODO: There's also a NETSCAPE buffer extension.
// http://www.vurdalakov.net/misc/gif/netscape-buffering-application-extension
if (subBlockSize == GifConstants.NetscapeLoopingSubBlockSize)
if (isXmp)
{
this.stream.Read(this.buffer, 0, GifConstants.NetscapeLoopingSubBlockSize);
this.gifMetadata.RepeatCount = GifNetscapeLoopingApplicationExtension.Parse(this.buffer.AsSpan(1)).RepeatCount;
this.stream.Skip(1); // Skip the terminator.
var extension = GifXmpApplicationExtension.Read(this.stream);
this.metadata.XmpProfile = new XmpProfile(extension.Data);
return;
}
else
{
int subBlockSize = this.stream.ReadByte();
// TODO: There's also a NETSCAPE buffer extension.
// http://www.vurdalakov.net/misc/gif/netscape-buffering-application-extension
if (subBlockSize == GifConstants.NetscapeLoopingSubBlockSize)
{
this.stream.Read(this.buffer, 0, GifConstants.NetscapeLoopingSubBlockSize);
this.gifMetadata.RepeatCount = GifNetscapeLoopingApplicationExtension.Parse(this.buffer.AsSpan(1)).RepeatCount;
this.stream.Skip(1); // Skip the terminator.
return;
}
// Could be something else not supported yet.
// Skip the subblock and terminator.
this.SkipBlock(subBlockSize);
}
// Could be XMP or something else not supported yet.
// Skip the subblock and terminator.
this.SkipBlock(subBlockSize);
return;
}

47
src/ImageSharp/Formats/Gif/GifEncoderCore.cs

@ -10,6 +10,7 @@ using System.Threading;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
@ -121,11 +122,8 @@ namespace SixLabors.ImageSharp.Formats.Gif
// Write the comments.
this.WriteComments(gifMetadata, stream);
// Write application extension to allow additional frames.
if (image.Frames.Count > 1)
{
this.WriteApplicationExtension(stream, gifMetadata.RepeatCount);
}
// Write application extensions.
this.WriteApplicationExtensions(stream, image.Frames.Count, gifMetadata.RepeatCount, metadata.XmpProfile);
if (useGlobalTable)
{
@ -326,15 +324,24 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// Writes the application extension to the stream.
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="frameCount">The frame count fo this image.</param>
/// <param name="repeatCount">The animated image repeat count.</param>
private void WriteApplicationExtension(Stream stream, ushort repeatCount)
/// <param name="xmpProfile">The XMP metadata profile. Null if profile is not to be written.</param>
private void WriteApplicationExtensions(Stream stream, int frameCount, ushort repeatCount, XmpProfile xmpProfile)
{
// Application Extension Header
if (repeatCount != 1)
// Application Extension: Loop repeat count.
if (frameCount > 1 && repeatCount != 1)
{
var loopingExtension = new GifNetscapeLoopingApplicationExtension(repeatCount);
this.WriteExtension(loopingExtension, stream);
}
// Application Extension: XMP Profile.
if (xmpProfile != null)
{
var xmpExtension = new GifXmpApplicationExtension(xmpProfile.Data);
this.WriteExtension(xmpExtension, stream);
}
}
/// <summary>
@ -420,14 +427,28 @@ namespace SixLabors.ImageSharp.Formats.Gif
private void WriteExtension<TGifExtension>(TGifExtension extension, Stream stream)
where TGifExtension : struct, IGifExtension
{
this.buffer[0] = GifConstants.ExtensionIntroducer;
this.buffer[1] = extension.Label;
IMemoryOwner<byte> owner = null;
Span<byte> buffer;
int extensionSize = extension.ContentLength;
if (extensionSize > this.buffer.Length - 3)
{
owner = this.memoryAllocator.Allocate<byte>(extensionSize + 3);
buffer = owner.GetSpan();
}
else
{
buffer = this.buffer;
}
buffer[0] = GifConstants.ExtensionIntroducer;
buffer[1] = extension.Label;
int extensionSize = extension.WriteTo(this.buffer.AsSpan(2));
extension.WriteTo(buffer.Slice(2));
this.buffer[extensionSize + 2] = GifConstants.Terminator;
buffer[extensionSize + 2] = GifConstants.Terminator;
stream.Write(this.buffer, 0, extensionSize + 3);
stream.Write(buffer, 0, extensionSize + 3);
owner?.Dispose();
}
/// <summary>

4
src/ImageSharp/Formats/Gif/Sections/GifGraphicControlExtension.cs

@ -1,4 +1,4 @@
// Copyright (c) Six Labors.
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
@ -63,6 +63,8 @@ namespace SixLabors.ImageSharp.Formats.Gif
byte IGifExtension.Label => GifConstants.GraphicControlLabel;
int IGifExtension.ContentLength => 5;
public int WriteTo(Span<byte> buffer)
{
ref GifGraphicControlExtension dest = ref Unsafe.As<byte, GifGraphicControlExtension>(ref MemoryMarshal.GetReference(buffer));

2
src/ImageSharp/Formats/Gif/Sections/GifNetscapeLoopingApplicationExtension.cs

@ -12,6 +12,8 @@ namespace SixLabors.ImageSharp.Formats.Gif
public byte Label => GifConstants.ApplicationExtensionLabel;
public int ContentLength => 16;
/// <summary>
/// Gets the repeat count.
/// 0 means loop indefinitely. Count is set as play n + 1 times.

97
src/ImageSharp/Formats/Gif/Sections/GifXmpApplicationExtension.cs

@ -0,0 +1,97 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Collections.Generic;
using System.IO;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
namespace SixLabors.ImageSharp.Formats.Gif
{
internal readonly struct GifXmpApplicationExtension : IGifExtension
{
public GifXmpApplicationExtension(byte[] data) => this.Data = data;
public byte Label => GifConstants.ApplicationExtensionLabel;
public int ContentLength => this.Data.Length + 269; // 12 + Data Length + 1 + 256
/// <summary>
/// Gets the raw Data.
/// </summary>
public byte[] Data { get; }
/// <summary>
/// Reads the XMP metadata from the specified stream.
/// </summary>
/// <param name="stream">The stream to read from.</param>
/// <returns>The XMP metadata</returns>
/// <exception cref="ImageFormatException">Thrown if the XMP block is not properly terminated.</exception>
public static GifXmpApplicationExtension Read(Stream stream)
{
// Read data in blocks, until an \0 character is encountered.
// We overshoot, indicated by the terminatorIndex variable.
const int bufferSize = 256;
var list = new List<byte[]>();
int terminationIndex = -1;
while (terminationIndex < 0)
{
byte[] temp = new byte[bufferSize];
int bytesRead = stream.Read(temp);
list.Add(temp);
terminationIndex = Array.IndexOf(temp, (byte)1);
}
// Pack all the blocks (except magic trailer) into one single array again.
int dataSize = ((list.Count - 1) * bufferSize) + terminationIndex;
byte[] buffer = new byte[dataSize];
Span<byte> bufferSpan = buffer;
int pos = 0;
for (int j = 0; j < list.Count - 1; j++)
{
list[j].CopyTo(bufferSpan.Slice(pos));
pos += bufferSize;
}
// Last one only needs the portion until terminationIndex copied over.
Span<byte> lastBytes = list[list.Count - 1];
lastBytes.Slice(0, terminationIndex).CopyTo(bufferSpan.Slice(pos));
// Skip the remainder of the magic trailer.
stream.Skip(258 - (bufferSize - terminationIndex));
return new GifXmpApplicationExtension(buffer);
}
public int WriteTo(Span<byte> buffer)
{
int totalSize = this.ContentLength;
if (buffer.Length < totalSize)
{
throw new InsufficientMemoryException("Unable to write XMP metadata to GIF image");
}
int bytesWritten = 0;
buffer[bytesWritten++] = GifConstants.ApplicationBlockSize;
// Write "XMP DataXMP"
ReadOnlySpan<byte> idBytes = GifConstants.XmpApplicationIdentificationBytes;
idBytes.CopyTo(buffer.Slice(bytesWritten));
bytesWritten += idBytes.Length;
// XMP Data itself
this.Data.CopyTo(buffer.Slice(bytesWritten));
bytesWritten += this.Data.Length;
// Write the Magic Trailer
buffer[bytesWritten++] = 0x01;
for (byte i = 255; i > 0; i--)
{
buffer[bytesWritten++] = i;
}
buffer[bytesWritten++] = 0x00;
return totalSize;
}
}
}

7
src/ImageSharp/Formats/Gif/Sections/IGifExtension.cs

@ -1,4 +1,4 @@
// Copyright (c) Six Labors.
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
@ -15,6 +15,11 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// </summary>
byte Label { get; }
/// <summary>
/// Gets the length of the contents of this extension.
/// </summary>
int ContentLength { get; }
/// <summary>
/// Writes the extension data to the buffer.
/// </summary>

30
src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs

@ -167,18 +167,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
int mcusPerLine = this.frame.McusPerLine;
ref HuffmanScanBuffer buffer = ref this.scanBuffer;
// Pre-derive the huffman table to avoid in-loop checks.
for (int i = 0; i < this.componentsCount; i++)
{
int order = this.frame.ComponentOrder[i];
JpegComponent component = this.components[order];
ref HuffmanTable dcHuffmanTable = ref this.dcHuffmanTables[component.DCHuffmanTableId];
ref HuffmanTable acHuffmanTable = ref this.acHuffmanTables[component.ACHuffmanTableId];
dcHuffmanTable.Configure();
acHuffmanTable.Configure();
}
for (int j = 0; j < mcusPerColumn; j++)
{
this.cancellationToken.ThrowIfCancellationRequested();
@ -248,8 +236,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
ref HuffmanTable dcHuffmanTable = ref this.dcHuffmanTables[component.DCHuffmanTableId];
ref HuffmanTable acHuffmanTable = ref this.acHuffmanTables[component.ACHuffmanTableId];
dcHuffmanTable.Configure();
acHuffmanTable.Configure();
for (int j = 0; j < h; j++)
{
@ -347,15 +333,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
int mcusPerLine = this.frame.McusPerLine;
ref HuffmanScanBuffer buffer = ref this.scanBuffer;
// Pre-derive the huffman table to avoid in-loop checks.
for (int k = 0; k < this.componentsCount; k++)
{
int order = this.frame.ComponentOrder[k];
JpegComponent component = this.components[order];
ref HuffmanTable dcHuffmanTable = ref this.dcHuffmanTables[component.DCHuffmanTableId];
dcHuffmanTable.Configure();
}
for (int j = 0; j < mcusPerColumn; j++)
{
for (int i = 0; i < mcusPerLine; i++)
@ -416,7 +393,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
if (this.SpectralStart == 0)
{
ref HuffmanTable dcHuffmanTable = ref this.dcHuffmanTables[component.DCHuffmanTableId];
dcHuffmanTable.Configure();
for (int j = 0; j < h; j++)
{
@ -444,7 +420,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
else
{
ref HuffmanTable acHuffmanTable = ref this.acHuffmanTables[component.ACHuffmanTableId];
acHuffmanTable.Configure();
for (int j = 0; j < h; j++)
{
@ -752,11 +727,12 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
/// <param name="index">Table index.</param>
/// <param name="codeLengths">Code lengths.</param>
/// <param name="values">Code values.</param>
/// <param name="workspace">The provided spare workspace memory, can be dirty.</param>
[MethodImpl(InliningOptions.ShortMethod)]
public void BuildHuffmanTable(int type, int index, ReadOnlySpan<byte> codeLengths, ReadOnlySpan<byte> values)
public void BuildHuffmanTable(int type, int index, ReadOnlySpan<byte> codeLengths, ReadOnlySpan<byte> values, Span<uint> workspace)
{
HuffmanTable[] tables = type == 0 ? this.dcHuffmanTables : this.acHuffmanTables;
tables[index] = new HuffmanTable(codeLengths, values);
tables[index] = new HuffmanTable(codeLengths, values, workspace);
}
}
}

76
src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs

@ -13,12 +13,10 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
[StructLayout(LayoutKind.Sequential)]
internal unsafe struct HuffmanTable
{
private bool isConfigured;
/// <summary>
/// Derived from the DHT marker. Sizes[k] = # of symbols with codes of length k bits; Sizes[0] is unused.
/// Memory workspace buffer size used in <see cref="HuffmanTable"/> ctor.
/// </summary>
public fixed byte Sizes[17];
public const int WorkspaceByteSize = 256 * sizeof(uint);
/// <summary>
/// Derived from the DHT marker. Contains the symbols, in order of incremental code length.
@ -58,51 +56,35 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
/// <summary>
/// Initializes a new instance of the <see cref="HuffmanTable"/> struct.
/// </summary>
/// <param name="codeLengths">The code lengths</param>
/// <param name="values">The huffman values</param>
public HuffmanTable(ReadOnlySpan<byte> codeLengths, ReadOnlySpan<byte> values)
/// <param name="codeLengths">The code lengths.</param>
/// <param name="values">The huffman values.</param>
/// <param name="workspace">The provided spare workspace memory, can be dirty.</param>
public HuffmanTable(ReadOnlySpan<byte> codeLengths, ReadOnlySpan<byte> values, Span<uint> workspace)
{
this.isConfigured = false;
Unsafe.CopyBlockUnaligned(ref this.Sizes[0], ref MemoryMarshal.GetReference(codeLengths), (uint)codeLengths.Length);
Unsafe.CopyBlockUnaligned(ref this.Values[0], ref MemoryMarshal.GetReference(values), (uint)values.Length);
}
/// <summary>
/// Expands the HuffmanTable into its readable form.
/// </summary>
public void Configure()
{
if (this.isConfigured)
{
return;
}
Span<char> huffSize = stackalloc char[257];
Span<uint> huffCode = stackalloc uint[257];
// Figure C.1: make table of Huffman code length for each symbol
// Generate codes
uint code = 0;
int si = 1;
int p = 0;
for (int j = 1; j <= 16; j++)
for (int i = 1; i <= 16; i++)
{
int i = this.Sizes[j];
while (i-- != 0)
int count = codeLengths[i];
for (int j = 0; j < count; j++)
{
huffSize[p++] = (char)j;
workspace[p++] = code;
code++;
}
}
huffSize[p] = (char)0;
// Figure C.2: generate the codes themselves
uint code = 0;
int si = huffSize[0];
p = 0;
while (huffSize[p] != 0)
{
while (huffSize[p] == si)
// 'code' is now 1 more than the last code used for codelength 'si'
// in the valid worst possible case 'code' would have the least
// significant bit set to 1, e.g. 1111(0) +1 => 1111(1)
// but it must still fit in 'si' bits since no huffman code can be equal to all 1s
// if last code is all ones, e.g. 1111(1), then incrementing it by 1 would yield
// a new code which occupies one extra bit, e.g. 1111(1) +1 => (1)1111(0)
if (code >= (1 << si))
{
huffCode[p++] = code;
code++;
JpegThrowHelper.ThrowInvalidImageContentException("Bad huffman table.");
}
code <<= 1;
@ -113,11 +95,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
p = 0;
for (int j = 1; j <= 16; j++)
{
if (this.Sizes[j] != 0)
if (codeLengths[j] != 0)
{
this.ValOffset[j] = p - (int)huffCode[p];
p += this.Sizes[j];
this.MaxCode[j] = huffCode[p - 1]; // Maximum code of length l
this.ValOffset[j] = p - (int)workspace[p];
p += codeLengths[j];
this.MaxCode[j] = workspace[p - 1]; // Maximum code of length l
this.MaxCode[j] <<= JpegConstants.Huffman.RegisterSize - j; // Left justify
this.MaxCode[j] |= (1ul << (JpegConstants.Huffman.RegisterSize - j)) - 1;
}
@ -142,11 +124,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
for (int length = 1; length <= JpegConstants.Huffman.LookupBits; length++)
{
int jShift = JpegConstants.Huffman.LookupBits - length;
for (int i = 1; i <= this.Sizes[length]; i++, p++)
for (int i = 1; i <= codeLengths[length]; i++, p++)
{
// length = current code's length, p = its index in huffCode[] & Values[].
// Generate left-justified code followed by all possible bit sequences
int lookBits = (int)(huffCode[p] << jShift);
int lookBits = (int)(workspace[p] << jShift);
for (int ctr = 1 << (JpegConstants.Huffman.LookupBits - length); ctr > 0; ctr--)
{
this.LookaheadSize[lookBits] = (byte)length;
@ -155,8 +137,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
}
}
}
this.isConfigured = true;
}
}
}

10
src/ImageSharp/Formats/Jpeg/Components/Decoder/JFifMarker.cs

@ -1,4 +1,4 @@
// Copyright (c) Six Labors.
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
@ -103,26 +103,22 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
/// <inheritdoc/>
public bool Equals(JFifMarker other)
{
return this.MajorVersion == other.MajorVersion
=> this.MajorVersion == other.MajorVersion
&& this.MinorVersion == other.MinorVersion
&& this.DensityUnits == other.DensityUnits
&& this.XDensity == other.XDensity
&& this.YDensity == other.YDensity;
}
/// <inheritdoc/>
public override bool Equals(object obj) => obj is JFifMarker other && this.Equals(other);
/// <inheritdoc/>
public override int GetHashCode()
{
return HashCode.Combine(
=> HashCode.Combine(
this.MajorVersion,
this.MinorVersion,
this.DensityUnits,
this.XDensity,
this.YDensity);
}
}
}

12
src/ImageSharp/Formats/Jpeg/Components/Decoder/ProfileResolver.cs

@ -60,6 +60,18 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
(byte)'E', (byte)'x', (byte)'i', (byte)'f', (byte)'\0', (byte)'\0'
};
/// <summary>
/// Gets the XMP specific markers.
/// </summary>
public static ReadOnlySpan<byte> XmpMarker => new[]
{
(byte)'h', (byte)'t', (byte)'t', (byte)'p', (byte)':', (byte)'/', (byte)'/',
(byte)'n', (byte)'s', (byte)'.', (byte)'a', (byte)'d', (byte)'o', (byte)'b',
(byte)'e', (byte)'.', (byte)'c', (byte)'o', (byte)'m', (byte)'/', (byte)'x',
(byte)'a', (byte)'p', (byte)'/', (byte)'1', (byte)'.', (byte)'0', (byte)'/',
(byte)0
};
/// <summary>
/// Gets the Adobe specific markers <see href="http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/JPEG.html#Adobe"/>.
/// </summary>

166
src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs

@ -17,6 +17,7 @@ using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Iptc;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Jpeg
@ -46,7 +47,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// <summary>
/// Whether the image has an EXIF marker.
/// </summary>
private bool isExif;
private bool hasExif;
/// <summary>
/// Contains exif data.
@ -56,7 +57,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// <summary>
/// Whether the image has an ICC marker.
/// </summary>
private bool isIcc;
private bool hasIcc;
/// <summary>
/// Contains ICC data.
@ -66,13 +67,23 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// <summary>
/// Whether the image has a IPTC data.
/// </summary>
private bool isIptc;
private bool hasIptc;
/// <summary>
/// Contains IPTC data.
/// </summary>
private byte[] iptcData;
/// <summary>
/// Whether the image has a XMP data.
/// </summary>
private bool hasXmp;
/// <summary>
/// Contains XMP data.
/// </summary>
private byte[] xmpData;
/// <summary>
/// Contains information about the JFIF marker.
/// </summary>
@ -183,6 +194,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
this.InitExifProfile();
this.InitIccProfile();
this.InitIptcProfile();
this.InitXmpProfile();
this.InitDerivedMetadataProperties();
return new Image<TPixel>(
@ -198,6 +210,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
this.InitExifProfile();
this.InitIccProfile();
this.InitIptcProfile();
this.InitXmpProfile();
this.InitDerivedMetadataProperties();
Size pixelSize = this.Frame.PixelSize;
@ -539,7 +552,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// </summary>
private void InitExifProfile()
{
if (this.isExif)
if (this.hasExif)
{
this.Metadata.ExifProfile = new ExifProfile(this.exifData);
}
@ -550,7 +563,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// </summary>
private void InitIccProfile()
{
if (this.isIcc)
if (this.hasIcc)
{
var profile = new IccProfile(this.iccData);
if (profile.CheckIsValid())
@ -565,13 +578,25 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// </summary>
private void InitIptcProfile()
{
if (this.isIptc)
if (this.hasIptc)
{
var profile = new IptcProfile(this.iptcData);
this.Metadata.IptcProfile = profile;
}
}
/// <summary>
/// Initializes the XMP profile.
/// </summary>
private void InitXmpProfile()
{
if (this.hasXmp)
{
var profile = new XmpProfile(this.xmpData);
this.Metadata.XmpProfile = profile;
}
}
/// <summary>
/// Assigns derived metadata properties to <see cref="Metadata"/>, eg. horizontal and vertical resolution if it has a JFIF header.
/// </summary>
@ -583,7 +608,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
this.Metadata.VerticalResolution = this.jFif.YDensity;
this.Metadata.ResolutionUnits = this.jFif.DensityUnits;
}
else if (this.isExif)
else if (this.hasExif)
{
double horizontalValue = this.GetExifResolutionValue(ExifTag.XResolution);
double verticalValue = this.GetExifResolutionValue(ExifTag.YResolution);
@ -625,7 +650,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
private void ProcessApplicationHeaderMarker(BufferedReadStream stream, int remaining)
{
// We can only decode JFif identifiers.
if (remaining < JFifMarker.Length)
// Some images contain multiple JFIF markers (Issue 1932) so we check to see
// if it's already been read.
if (remaining < JFifMarker.Length || (!this.jFif.Equals(default)))
{
// Skip the application header length
stream.Skip(remaining);
@ -656,8 +683,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// <param name="remaining">The remaining bytes in the segment block.</param>
private void ProcessApp1Marker(BufferedReadStream stream, int remaining)
{
const int Exif00 = 6;
if (remaining < Exif00 || this.IgnoreMetadata)
const int ExifMarkerLength = 6;
const int XmpMarkerLength = 29;
if (remaining < ExifMarkerLength || this.IgnoreMetadata)
{
// Skip the application header length
stream.Skip(remaining);
@ -669,23 +697,55 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
JpegThrowHelper.ThrowInvalidImageContentException("Bad App1 Marker length.");
}
byte[] profile = new byte[remaining];
stream.Read(profile, 0, remaining);
// XMP marker is the longest, so read at least that many bytes into temp.
stream.Read(this.temp, 0, ExifMarkerLength);
if (ProfileResolver.IsProfile(profile, ProfileResolver.ExifMarker))
if (ProfileResolver.IsProfile(this.temp, ProfileResolver.ExifMarker))
{
this.isExif = true;
remaining -= ExifMarkerLength;
this.hasExif = true;
byte[] profile = new byte[remaining];
stream.Read(profile, 0, remaining);
if (this.exifData is null)
{
// The first 6 bytes (Exif00) will be skipped, because this is Jpeg specific
this.exifData = profile.AsSpan(Exif00).ToArray();
this.exifData = profile;
}
else
{
// If the EXIF information exceeds 64K, it will be split over multiple APP1 markers
this.ExtendProfile(ref this.exifData, profile.AsSpan(Exif00).ToArray());
this.ExtendProfile(ref this.exifData, profile);
}
remaining = 0;
}
if (ProfileResolver.IsProfile(this.temp, ProfileResolver.XmpMarker.Slice(0, ExifMarkerLength)))
{
stream.Read(this.temp, 0, XmpMarkerLength - ExifMarkerLength);
remaining -= XmpMarkerLength;
if (ProfileResolver.IsProfile(this.temp, ProfileResolver.XmpMarker.Slice(ExifMarkerLength)))
{
this.hasXmp = true;
byte[] profile = new byte[remaining];
stream.Read(profile, 0, remaining);
if (this.xmpData is null)
{
this.xmpData = profile;
}
else
{
// If the XMP information exceeds 64K, it will be split over multiple APP1 markers
this.ExtendProfile(ref this.xmpData, profile);
}
remaining = 0;
}
}
// Skip over any remaining bytes of this header.
stream.Skip(remaining);
}
/// <summary>
@ -709,7 +769,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
if (ProfileResolver.IsProfile(identifier, ProfileResolver.IccMarker))
{
this.isIcc = true;
this.hasIcc = true;
byte[] profile = new byte[remaining];
stream.Read(profile, 0, remaining);
@ -768,7 +828,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
int dataStartIdx = 2 + resourceBlockNameLength + 4;
if (resourceDataSize > 0 && blockDataSpan.Length >= dataStartIdx + resourceDataSize)
{
this.isIptc = true;
this.hasIptc = true;
this.iptcData = blockDataSpan.Slice(dataStartIdx, resourceDataSize).ToArray();
break;
}
@ -788,6 +848,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
}
}
}
else
{
// If the profile is unknown skip over the rest of it.
stream.Skip(remaining);
}
}
/// <summary>
@ -1095,12 +1160,18 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// <param name="remaining">The remaining bytes in the segment block.</param>
private void ProcessDefineHuffmanTablesMarker(BufferedReadStream stream, int remaining)
{
int length = remaining;
const int codeLengthsByteSize = 17;
const int codeValuesMaxByteSize = 256;
const int totalBufferSize = codeLengthsByteSize + codeValuesMaxByteSize + HuffmanTable.WorkspaceByteSize;
using (IMemoryOwner<byte> huffmanData = this.Configuration.MemoryAllocator.Allocate<byte>(256, AllocationOptions.Clean))
int length = remaining;
using (IMemoryOwner<byte> buffer = this.Configuration.MemoryAllocator.Allocate<byte>(totalBufferSize))
{
Span<byte> huffmanDataSpan = huffmanData.GetSpan();
ref byte huffmanDataRef = ref MemoryMarshal.GetReference(huffmanDataSpan);
Span<byte> bufferSpan = buffer.GetSpan();
Span<byte> huffmanLegthsSpan = bufferSpan.Slice(0, codeLengthsByteSize);
Span<byte> huffmanValuesSpan = bufferSpan.Slice(codeLengthsByteSize, codeValuesMaxByteSize);
Span<uint> tableWorkspace = MemoryMarshal.Cast<byte, uint>(bufferSpan.Slice(codeLengthsByteSize + codeValuesMaxByteSize));
for (int i = 2; i < remaining;)
{
byte huffmanTableSpec = (byte)stream.ReadByte();
@ -1110,49 +1181,40 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
// Types 0..1 DC..AC
if (tableType > 1)
{
JpegThrowHelper.ThrowInvalidImageContentException($"Bad huffman table type: {tableType}");
JpegThrowHelper.ThrowInvalidImageContentException($"Bad huffman table type: {tableType}.");
}
// Max tables of each type
if (tableIndex > 3)
{
JpegThrowHelper.ThrowInvalidImageContentException($"Bad huffman table index: {tableIndex}");
JpegThrowHelper.ThrowInvalidImageContentException($"Bad huffman table index: {tableIndex}.");
}
stream.Read(huffmanDataSpan, 0, 16);
stream.Read(huffmanLegthsSpan, 1, 16);
using (IMemoryOwner<byte> codeLengths = this.Configuration.MemoryAllocator.Allocate<byte>(17, AllocationOptions.Clean))
int codeLengthSum = 0;
for (int j = 1; j < 17; j++)
{
Span<byte> codeLengthsSpan = codeLengths.GetSpan();
ref byte codeLengthsRef = ref MemoryMarshal.GetReference(codeLengthsSpan);
int codeLengthSum = 0;
for (int j = 1; j < 17; j++)
{
codeLengthSum += Unsafe.Add(ref codeLengthsRef, j) = Unsafe.Add(ref huffmanDataRef, j - 1);
}
codeLengthSum += huffmanLegthsSpan[j];
}
length -= 17;
length -= 17;
if (codeLengthSum > 256 || codeLengthSum > length)
{
JpegThrowHelper.ThrowInvalidImageContentException("Huffman table has excessive length.");
}
if (codeLengthSum > 256 || codeLengthSum > length)
{
JpegThrowHelper.ThrowInvalidImageContentException("Huffman table has excessive length.");
}
using (IMemoryOwner<byte> huffmanValues = this.Configuration.MemoryAllocator.Allocate<byte>(256, AllocationOptions.Clean))
{
Span<byte> huffmanValuesSpan = huffmanValues.GetSpan();
stream.Read(huffmanValuesSpan, 0, codeLengthSum);
stream.Read(huffmanValuesSpan, 0, codeLengthSum);
i += 17 + codeLengthSum;
i += 17 + codeLengthSum;
this.scanDecoder.BuildHuffmanTable(
tableType,
tableIndex,
codeLengthsSpan,
huffmanValuesSpan);
}
}
this.scanDecoder.BuildHuffmanTable(
tableType,
tableIndex,
huffmanLegthsSpan,
huffmanValuesSpan.Slice(0, codeLengthSum),
tableWorkspace);
}
}
}

57
src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs

@ -14,6 +14,7 @@ using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Iptc;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Jpeg
@ -109,7 +110,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
this.WriteJfifApplicationHeader(metadata);
}
// Write Exif, ICC and IPTC profiles
// Write Exif, XMP, ICC and IPTC profiles
this.WriteProfiles(metadata);
if (this.colorType == JpegColorType.Rgb)
@ -466,6 +467,54 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
this.outputStream.Write(data, 0, data.Length);
}
/// <summary>
/// Writes the XMP metadata.
/// </summary>
/// <param name="xmpProfile">The XMP metadata to write.</param>
/// <exception cref="ImageFormatException">
/// Thrown if the XMP profile size exceeds the limit of 65533 bytes.
/// </exception>
private void WriteXmpProfile(XmpProfile xmpProfile)
{
if (xmpProfile is null)
{
return;
}
const int XmpOverheadLength = 29;
const int Max = 65533;
const int MaxData = Max - XmpOverheadLength;
byte[] data = xmpProfile.Data;
if (data is null || data.Length == 0)
{
return;
}
int dataLength = data.Length;
int offset = 0;
while (dataLength > 0)
{
int length = dataLength; // Number of bytes to write.
if (length > MaxData)
{
length = MaxData;
}
dataLength -= length;
int app1Length = 2 + ProfileResolver.XmpMarker.Length + length;
this.WriteApp1Header(app1Length);
this.outputStream.Write(ProfileResolver.XmpMarker);
this.outputStream.Write(data, offset, length);
offset += length;
}
}
/// <summary>
/// Writes the App1 header.
/// </summary>
@ -579,8 +628,14 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
return;
}
// For compatibility, place the profiles in the following order:
// - APP1 EXIF
// - APP1 XMP
// - APP2 ICC
// - APP13 IPTC
metadata.SyncProfiles();
this.WriteExifProfile(metadata.ExifProfile);
this.WriteXmpProfile(metadata.XmpProfile);
this.WriteIccProfile(metadata.IccProfile);
this.WriteIptcProfile(metadata.IptcProfile);
}

13
src/ImageSharp/Formats/PixelTypeInfo.cs

@ -15,8 +15,17 @@ namespace SixLabors.ImageSharp.Formats
/// Initializes a new instance of the <see cref="PixelTypeInfo"/> class.
/// </summary>
/// <param name="bitsPerPixel">Color depth, in number of bits per pixel.</param>
/// <param name="alpha">Tthe pixel alpha transparency behavior.</param>
internal PixelTypeInfo(int bitsPerPixel, PixelAlphaRepresentation? alpha = null)
public PixelTypeInfo(int bitsPerPixel)
{
this.BitsPerPixel = bitsPerPixel;
}
/// <summary>
/// Initializes a new instance of the <see cref="PixelTypeInfo"/> class.
/// </summary>
/// <param name="bitsPerPixel">Color depth, in number of bits per pixel.</param>
/// <param name="alpha">The pixel alpha transparency behavior.</param>
public PixelTypeInfo(int bitsPerPixel, PixelAlphaRepresentation alpha)
{
this.BitsPerPixel = bitsPerPixel;
this.AlphaRepresentation = alpha;

24
src/ImageSharp/Formats/Png/PngConstants.cs

@ -78,5 +78,29 @@ namespace SixLabors.ImageSharp.Formats.Png
0x1A, // EOF
0x0A // LF
};
/// <summary>
/// Gets the keyword of the XMP metadata, encoded in an iTXT chunk.
/// </summary>
public static ReadOnlySpan<byte> XmpKeyword => new byte[]
{
(byte)'X',
(byte)'M',
(byte)'L',
(byte)':',
(byte)'c',
(byte)'o',
(byte)'m',
(byte)'.',
(byte)'a',
(byte)'d',
(byte)'o',
(byte)'b',
(byte)'e',
(byte)'.',
(byte)'x',
(byte)'m',
(byte)'p'
};
}
}

19
src/ImageSharp/Formats/Png/PngDecoderCore.cs

@ -19,6 +19,7 @@ using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Png
@ -194,7 +195,7 @@ namespace SixLabors.ImageSharp.Formats.Png
this.ReadCompressedTextChunk(metadata, pngMetadata, chunk.Data.GetSpan());
break;
case PngChunkType.InternationalText:
this.ReadInternationalTextChunk(pngMetadata, chunk.Data.GetSpan());
this.ReadInternationalTextChunk(metadata, chunk.Data.GetSpan());
break;
case PngChunkType.Exif:
if (!this.ignoreMetadata)
@ -316,7 +317,7 @@ namespace SixLabors.ImageSharp.Formats.Png
break;
}
this.ReadInternationalTextChunk(pngMetadata, chunk.Data.GetSpan());
this.ReadInternationalTextChunk(metadata, chunk.Data.GetSpan());
break;
case PngChunkType.Exif:
if (this.colorMetadataOnly)
@ -1224,13 +1225,14 @@ namespace SixLabors.ImageSharp.Formats.Png
/// </summary>
/// <param name="metadata">The metadata to decode to.</param>
/// <param name="data">The <see cref="T:Span"/> containing the data.</param>
private void ReadInternationalTextChunk(PngMetadata metadata, ReadOnlySpan<byte> data)
private void ReadInternationalTextChunk(ImageMetadata metadata, ReadOnlySpan<byte> data)
{
if (this.ignoreMetadata)
{
return;
}
PngMetadata pngMetadata = metadata.GetPngMetadata();
int zeroIndexKeyword = data.IndexOf((byte)0);
if (zeroIndexKeyword < PngConstants.MinTextKeywordLength || zeroIndexKeyword > PngConstants.MaxTextKeywordLength)
{
@ -1276,13 +1278,18 @@ namespace SixLabors.ImageSharp.Formats.Png
if (this.TryUncompressTextData(compressedData, PngConstants.TranslatedEncoding, out string uncompressed))
{
metadata.TextData.Add(new PngTextData(keyword, uncompressed, language, translatedKeyword));
pngMetadata.TextData.Add(new PngTextData(keyword, uncompressed, language, translatedKeyword));
}
}
else if (this.IsXmpTextData(keywordBytes))
{
XmpProfile xmpProfile = new XmpProfile(data.Slice(dataStartIdx).ToArray());
metadata.XmpProfile = xmpProfile;
}
else
{
string value = PngConstants.TranslatedEncoding.GetString(data.Slice(dataStartIdx));
metadata.TextData.Add(new PngTextData(keyword, value, language, translatedKeyword));
pngMetadata.TextData.Add(new PngTextData(keyword, value, language, translatedKeyword));
}
}
@ -1550,6 +1557,8 @@ namespace SixLabors.ImageSharp.Formats.Png
return true;
}
private bool IsXmpTextData(ReadOnlySpan<byte> keywordBytes) => keywordBytes.SequenceEqual(PngConstants.XmpKeyword);
private void SwapScanlineBuffers()
{
IMemoryOwner<byte> temp = this.previousScanline;

115
src/ImageSharp/Formats/Png/PngEncoderCore.cs

@ -138,6 +138,7 @@ namespace SixLabors.ImageSharp.Formats.Png
this.WriteTransparencyChunk(stream, pngMetadata);
this.WritePhysicalChunk(stream, metadata);
this.WriteExifChunk(stream, metadata);
this.WriteXmpChunk(stream, metadata);
this.WriteTextChunks(stream, pngMetadata);
this.WriteDataChunks(clearTransparency ? clonedImage : image, quantized, stream);
this.WriteEndChunk(stream);
@ -654,6 +655,51 @@ namespace SixLabors.ImageSharp.Formats.Png
this.WriteChunk(stream, PngChunkType.Exif, meta.ExifProfile.ToByteArray());
}
/// <summary>
/// Writes an iTXT chunk, containing the XMP metdata to the stream, if such profile is present in the metadata.
/// </summary>
/// <param name="stream">The <see cref="Stream"/> containing image data.</param>
/// <param name="meta">The image metadata.</param>
private void WriteXmpChunk(Stream stream, ImageMetadata meta)
{
const int iTxtHeaderSize = 5;
if (((this.options.ChunkFilter ?? PngChunkFilter.None) & PngChunkFilter.ExcludeTextChunks) == PngChunkFilter.ExcludeTextChunks)
{
return;
}
if (meta.XmpProfile is null)
{
return;
}
var xmpData = meta.XmpProfile.Data;
if (xmpData.Length == 0)
{
return;
}
int payloadLength = xmpData.Length + PngConstants.XmpKeyword.Length + iTxtHeaderSize;
using (IMemoryOwner<byte> owner = this.memoryAllocator.Allocate<byte>(payloadLength))
{
Span<byte> payload = owner.GetSpan();
PngConstants.XmpKeyword.CopyTo(payload);
int bytesWritten = PngConstants.XmpKeyword.Length;
// Write the iTxt header (all zeros in this case)
payload[bytesWritten++] = 0;
payload[bytesWritten++] = 0;
payload[bytesWritten++] = 0;
payload[bytesWritten++] = 0;
payload[bytesWritten++] = 0;
// And the XMP data itself
xmpData.CopyTo(payload.Slice(bytesWritten));
this.WriteChunk(stream, PngChunkType.InternationalText, payload);
}
}
/// <summary>
/// Writes a text chunk to the stream. Can be either a tTXt, iTXt or zTXt chunk,
/// depending whether the text contains any latin characters or should be compressed.
@ -693,21 +739,33 @@ namespace SixLabors.ImageSharp.Formats.Png
byte[] translatedKeyword = PngConstants.TranslatedEncoding.GetBytes(textData.TranslatedKeyword);
byte[] languageTag = PngConstants.LanguageEncoding.GetBytes(textData.LanguageTag);
Span<byte> outputBytes = new byte[keywordBytes.Length + textBytes.Length +
translatedKeyword.Length + languageTag.Length + 5];
keywordBytes.CopyTo(outputBytes);
if (textData.Value.Length > this.options.TextCompressionThreshold)
int payloadLength = keywordBytes.Length + textBytes.Length + translatedKeyword.Length + languageTag.Length + 5;
using (IMemoryOwner<byte> owner = this.memoryAllocator.Allocate<byte>(payloadLength))
{
// Indicate that the text is compressed.
outputBytes[keywordBytes.Length + 1] = 1;
}
Span<byte> outputBytes = owner.GetSpan();
keywordBytes.CopyTo(outputBytes);
int bytesWritten = keywordBytes.Length;
outputBytes[bytesWritten++] = 0;
if (textData.Value.Length > this.options.TextCompressionThreshold)
{
// Indicate that the text is compressed.
outputBytes[bytesWritten++] = 1;
}
else
{
outputBytes[bytesWritten++] = 0;
}
int keywordStart = keywordBytes.Length + 3;
languageTag.CopyTo(outputBytes.Slice(keywordStart));
int translatedKeywordStart = keywordStart + languageTag.Length + 1;
translatedKeyword.CopyTo(outputBytes.Slice(translatedKeywordStart));
textBytes.CopyTo(outputBytes.Slice(translatedKeywordStart + translatedKeyword.Length + 1));
this.WriteChunk(stream, PngChunkType.InternationalText, outputBytes.ToArray());
outputBytes[bytesWritten++] = 0;
languageTag.CopyTo(outputBytes.Slice(bytesWritten));
bytesWritten += languageTag.Length;
outputBytes[bytesWritten++] = 0;
translatedKeyword.CopyTo(outputBytes.Slice(bytesWritten));
bytesWritten += translatedKeyword.Length;
outputBytes[bytesWritten++] = 0;
textBytes.CopyTo(outputBytes.Slice(bytesWritten));
this.WriteChunk(stream, PngChunkType.InternationalText, outputBytes);
}
}
else
{
@ -716,19 +774,32 @@ namespace SixLabors.ImageSharp.Formats.Png
// Write zTXt chunk.
byte[] compressedData =
this.GetCompressedTextBytes(PngConstants.Encoding.GetBytes(textData.Value));
Span<byte> outputBytes = new byte[textData.Keyword.Length + compressedData.Length + 2];
PngConstants.Encoding.GetBytes(textData.Keyword).CopyTo(outputBytes);
compressedData.CopyTo(outputBytes.Slice(textData.Keyword.Length + 2));
this.WriteChunk(stream, PngChunkType.CompressedText, outputBytes.ToArray());
int payloadLength = textData.Keyword.Length + compressedData.Length + 2;
using (IMemoryOwner<byte> owner = this.memoryAllocator.Allocate<byte>(payloadLength))
{
Span<byte> outputBytes = owner.GetSpan();
PngConstants.Encoding.GetBytes(textData.Keyword).CopyTo(outputBytes);
int bytesWritten = textData.Keyword.Length;
outputBytes[bytesWritten++] = 0;
outputBytes[bytesWritten++] = 0;
compressedData.CopyTo(outputBytes.Slice(bytesWritten));
this.WriteChunk(stream, PngChunkType.CompressedText, outputBytes.ToArray());
}
}
else
{
// Write tEXt chunk.
Span<byte> outputBytes = new byte[textData.Keyword.Length + textData.Value.Length + 1];
PngConstants.Encoding.GetBytes(textData.Keyword).CopyTo(outputBytes);
PngConstants.Encoding.GetBytes(textData.Value)
.CopyTo(outputBytes.Slice(textData.Keyword.Length + 1));
this.WriteChunk(stream, PngChunkType.Text, outputBytes.ToArray());
int payloadLength = textData.Keyword.Length + textData.Value.Length + 1;
using (IMemoryOwner<byte> owner = this.memoryAllocator.Allocate<byte>(payloadLength))
{
Span<byte> outputBytes = owner.GetSpan();
PngConstants.Encoding.GetBytes(textData.Keyword).CopyTo(outputBytes);
int bytesWritten = textData.Keyword.Length;
outputBytes[bytesWritten++] = 0;
PngConstants.Encoding.GetBytes(textData.Value)
.CopyTo(outputBytes.Slice(bytesWritten));
this.WriteChunk(stream, PngChunkType.Text, outputBytes.ToArray());
}
}
}
}

9
src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs

@ -13,6 +13,7 @@ using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Tiff
@ -204,9 +205,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff
private ImageFrame<TPixel> DecodeFrame<TPixel>(ExifProfile tags, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
ImageFrameMetadata imageFrameMetaData = this.ignoreMetadata ?
new ImageFrameMetadata() :
new ImageFrameMetadata { ExifProfile = tags, XmpProfile = tags.GetValue(ExifTag.XMP)?.Value };
var imageFrameMetaData = new ImageFrameMetadata();
if (!this.ignoreMetadata)
{
imageFrameMetaData.ExifProfile = tags;
}
TiffFrameMetadata tiffFrameMetaData = imageFrameMetaData.GetTiffMetadata();
TiffFrameMetadata.Parse(tiffFrameMetaData, tags);

7
src/ImageSharp/Formats/Tiff/TiffDecoderMetadataCreator.cs

@ -9,6 +9,7 @@ using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Iptc;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Tiff
@ -39,6 +40,12 @@ namespace SixLabors.ImageSharp.Formats.Tiff
frameMetaData.IptcProfile = new IptcProfile(iptcBytes);
}
IExifValue<byte[]> xmpProfileBytes = frameMetaData.ExifProfile.GetValue(ExifTag.XMP);
if (xmpProfileBytes != null)
{
frameMetaData.XmpProfile = new XmpProfile(xmpProfileBytes.Value);
}
IExifValue<byte[]> iccProfileBytes = frameMetaData.ExifProfile.GetValue(ExifTag.IccProfile);
if (iccProfileBytes != null)
{

9
src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs

@ -6,6 +6,7 @@ using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Formats.Tiff.Constants;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
namespace SixLabors.ImageSharp.Formats.Tiff
{
@ -57,9 +58,9 @@ namespace SixLabors.ImageSharp.Formats.Tiff
{
ImageFrame rootFrame = image.Frames.RootFrame;
ExifProfile rootFrameExifProfile = rootFrame.Metadata.ExifProfile ?? new ExifProfile();
byte[] foorFrameXmpBytes = rootFrame.Metadata.XmpProfile;
XmpProfile rootFrameXmpProfile = rootFrame.Metadata.XmpProfile;
this.ProcessProfiles(image.Metadata, rootFrameExifProfile, foorFrameXmpBytes);
this.ProcessProfiles(image.Metadata, rootFrameExifProfile, rootFrameXmpProfile);
this.ProcessMetadata(rootFrameExifProfile);
if (!this.Collector.Entries.Exists(t => t.Tag == ExifTag.Software))
@ -149,7 +150,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff
}
}
private void ProcessProfiles(ImageMetadata imageMetadata, ExifProfile exifProfile, byte[] xmpProfile)
private void ProcessProfiles(ImageMetadata imageMetadata, ExifProfile exifProfile, XmpProfile xmpProfile)
{
if (exifProfile != null && exifProfile.Parts != ExifParts.None)
{
@ -203,7 +204,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff
{
var xmp = new ExifByteArray(ExifTagValue.XMP, ExifDataType.Byte)
{
Value = xmpProfile
Value = xmpProfile.Data
};
this.Collector.Add(xmp);

37
src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs

@ -5,6 +5,7 @@ using System;
using System.Buffers.Binary;
using System.IO;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
namespace SixLabors.ImageSharp.Formats.Webp.BitWriter
{
@ -90,34 +91,35 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter
}
/// <summary>
/// Calculates the exif chunk size.
/// Calculates the chunk size of EXIF or XMP metadata.
/// </summary>
/// <param name="exifBytes">The exif profile bytes.</param>
/// <param name="metadataBytes">The metadata profile bytes.</param>
/// <returns>The exif chunk size in bytes.</returns>
protected uint ExifChunkSize(byte[] exifBytes)
protected uint MetadataChunkSize(byte[] metadataBytes)
{
uint exifSize = (uint)exifBytes.Length;
uint exifChunkSize = WebpConstants.ChunkHeaderSize + exifSize + (exifSize & 1);
uint metaSize = (uint)metadataBytes.Length;
uint metaChunkSize = WebpConstants.ChunkHeaderSize + metaSize + (metaSize & 1);
return exifChunkSize;
return metaChunkSize;
}
/// <summary>
/// Writes the Exif profile to the stream.
/// Writes a metadata profile (EXIF or XMP) to the stream.
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="exifBytes">The exif profile bytes.</param>
protected void WriteExifProfile(Stream stream, byte[] exifBytes)
/// <param name="metadataBytes">The metadata profile's bytes.</param>
/// <param name="chunkType">The chuck type to write.</param>
protected void WriteMetadataProfile(Stream stream, byte[] metadataBytes, WebpChunkType chunkType)
{
DebugGuard.NotNull(exifBytes, nameof(exifBytes));
DebugGuard.NotNull(metadataBytes, nameof(metadataBytes));
uint size = (uint)exifBytes.Length;
uint size = (uint)metadataBytes.Length;
Span<byte> buf = this.scratchBuffer.AsSpan(0, 4);
BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Exif);
BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)chunkType);
stream.Write(buf);
BinaryPrimitives.WriteUInt32LittleEndian(buf, size);
stream.Write(buf);
stream.Write(exifBytes);
stream.Write(metadataBytes);
// Add padding byte if needed.
if ((size & 1) == 1)
@ -131,10 +133,11 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="exifProfile">A exif profile or null, if it does not exist.</param>
/// <param name="xmpProfile">A XMP profile or null, if it does not exist.</param>
/// <param name="width">The width of the image.</param>
/// <param name="height">The height of the image.</param>
/// <param name="hasAlpha">Flag indicating, if a alpha channel is present.</param>
protected void WriteVp8XHeader(Stream stream, ExifProfile exifProfile, uint width, uint height, bool hasAlpha)
protected void WriteVp8XHeader(Stream stream, ExifProfile exifProfile, XmpProfile xmpProfile, uint width, uint height, bool hasAlpha)
{
if (width > MaxDimension || height > MaxDimension)
{
@ -154,6 +157,12 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter
flags |= 8;
}
if (xmpProfile != null)
{
// Set xmp bit.
flags |= 4;
}
if (hasAlpha)
{
// Set alpha bit.

28
src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs

@ -6,6 +6,7 @@ using System.Buffers.Binary;
using System.IO;
using SixLabors.ImageSharp.Formats.Webp.Lossy;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
namespace SixLabors.ImageSharp.Formats.Webp.BitWriter
{
@ -404,20 +405,30 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="exifProfile">The exif profile.</param>
/// <param name="xmpProfile">The XMP profile.</param>
/// <param name="width">The width of the image.</param>
/// <param name="height">The height of the image.</param>
/// <param name="hasAlpha">Flag indicating, if a alpha channel is present.</param>
public void WriteEncodedImageToStream(Stream stream, ExifProfile exifProfile, uint width, uint height, bool hasAlpha)
public void WriteEncodedImageToStream(Stream stream, ExifProfile exifProfile, XmpProfile xmpProfile, uint width, uint height, bool hasAlpha)
{
bool isVp8X = false;
byte[] exifBytes = null;
byte[] xmpBytes = null;
uint riffSize = 0;
if (exifProfile != null)
{
isVp8X = true;
riffSize += ExtendedFileChunkSize;
exifBytes = exifProfile.ToByteArray();
riffSize += this.ExifChunkSize(exifBytes);
riffSize += this.MetadataChunkSize(exifBytes);
}
if (xmpProfile != null)
{
isVp8X = true;
riffSize += ExtendedFileChunkSize;
xmpBytes = xmpProfile.Data;
riffSize += this.MetadataChunkSize(xmpBytes);
}
this.Finish();
@ -440,7 +451,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter
riffSize += WebpConstants.TagSize + WebpConstants.ChunkHeaderSize + vp8Size;
// Emit headers and partition #0
this.WriteWebpHeaders(stream, size0, vp8Size, riffSize, isVp8X, width, height, exifProfile, hasAlpha);
this.WriteWebpHeaders(stream, size0, vp8Size, riffSize, isVp8X, width, height, exifProfile, xmpProfile, hasAlpha);
bitWriterPartZero.WriteToStream(stream);
// Write the encoded image to the stream.
@ -452,7 +463,12 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter
if (exifProfile != null)
{
this.WriteExifProfile(stream, exifBytes);
this.WriteMetadataProfile(stream, exifBytes, WebpChunkType.Exif);
}
if (xmpProfile != null)
{
this.WriteMetadataProfile(stream, xmpBytes, WebpChunkType.Xmp);
}
}
@ -623,14 +639,14 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter
while (it.Next());
}
private void WriteWebpHeaders(Stream stream, uint size0, uint vp8Size, uint riffSize, bool isVp8X, uint width, uint height, ExifProfile exifProfile, bool hasAlpha)
private void WriteWebpHeaders(Stream stream, uint size0, uint vp8Size, uint riffSize, bool isVp8X, uint width, uint height, ExifProfile exifProfile, XmpProfile xmpProfile, bool hasAlpha)
{
this.WriteRiffHeader(stream, riffSize);
// Write VP8X, header if necessary.
if (isVp8X)
{
this.WriteVp8XHeader(stream, exifProfile, width, height, hasAlpha);
this.WriteVp8XHeader(stream, exifProfile, xmpProfile, width, height, hasAlpha);
}
this.WriteVp8Header(stream, vp8Size);

24
src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs

@ -6,6 +6,7 @@ using System.Buffers.Binary;
using System.IO;
using SixLabors.ImageSharp.Formats.Webp.Lossless;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
namespace SixLabors.ImageSharp.Formats.Webp.BitWriter
{
@ -132,20 +133,30 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="exifProfile">The exif profile.</param>
/// <param name="xmpProfile">The XMP profile.</param>
/// <param name="width">The width of the image.</param>
/// <param name="height">The height of the image.</param>
/// <param name="hasAlpha">Flag indicating, if a alpha channel is present.</param>
public void WriteEncodedImageToStream(Stream stream, ExifProfile exifProfile, uint width, uint height, bool hasAlpha)
public void WriteEncodedImageToStream(Stream stream, ExifProfile exifProfile, XmpProfile xmpProfile, uint width, uint height, bool hasAlpha)
{
bool isVp8X = false;
byte[] exifBytes = null;
byte[] xmpBytes = null;
uint riffSize = 0;
if (exifProfile != null)
{
isVp8X = true;
riffSize += ExtendedFileChunkSize;
exifBytes = exifProfile.ToByteArray();
riffSize += this.ExifChunkSize(exifBytes);
riffSize += this.MetadataChunkSize(exifBytes);
}
if (xmpProfile != null)
{
isVp8X = true;
riffSize += ExtendedFileChunkSize;
xmpBytes = xmpProfile.Data;
riffSize += this.MetadataChunkSize(xmpBytes);
}
this.Finish();
@ -160,7 +171,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter
// Write VP8X, header if necessary.
if (isVp8X)
{
this.WriteVp8XHeader(stream, exifProfile, width, height, hasAlpha);
this.WriteVp8XHeader(stream, exifProfile, xmpProfile, width, height, hasAlpha);
}
// Write magic bytes indicating its a lossless webp.
@ -180,7 +191,12 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter
if (exifProfile != null)
{
this.WriteExifProfile(stream, exifBytes);
this.WriteMetadataProfile(stream, exifBytes, WebpChunkType.Exif);
}
if (xmpProfile != null)
{
this.WriteMetadataProfile(stream, xmpBytes, WebpChunkType.Xmp);
}
}

5
src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs

@ -10,6 +10,7 @@ using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Formats.Webp.BitWriter;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Webp.Lossless
@ -252,7 +253,9 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossless
this.EncodeStream(image);
// Write bytes from the bitwriter buffer to the stream.
this.bitWriter.WriteEncodedImageToStream(stream, image.Metadata.ExifProfile, (uint)width, (uint)height, hasAlpha);
ImageMetadata metadata = image.Metadata;
metadata.SyncProfiles();
this.bitWriter.WriteEncodedImageToStream(stream, metadata.ExifProfile, metadata.XmpProfile, (uint)width, (uint)height, hasAlpha);
}
/// <summary>

6
src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs

@ -7,6 +7,7 @@ using System.IO;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Formats.Webp.BitWriter;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Webp.Lossy
@ -355,8 +356,9 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy
this.AdjustFilterStrength();
// Write bytes from the bitwriter buffer to the stream.
image.Metadata.SyncProfiles();
this.bitWriter.WriteEncodedImageToStream(stream, image.Metadata.ExifProfile, (uint)width, (uint)height, hasAlpha);
ImageMetadata metadata = image.Metadata;
metadata.SyncProfiles();
this.bitWriter.WriteEncodedImageToStream(stream, metadata.ExifProfile, metadata.XmpProfile, (uint)width, (uint)height, hasAlpha);
}
/// <inheritdoc/>

78
src/ImageSharp/Formats/Webp/WebpDecoderCore.cs

@ -13,6 +13,7 @@ using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Webp
@ -177,6 +178,7 @@ namespace SixLabors.ImageSharp.Formats.Webp
/// Reads an the extended webp file header. An extended file header consists of:
/// - A 'VP8X' chunk with information about features used in the file.
/// - An optional 'ICCP' chunk with color profile.
/// - An optional 'XMP' chunk with metadata.
/// - An optional 'ANIM' chunk with animation control data.
/// - An optional 'ALPH' chunk with alpha channel data.
/// After the image header, image data will follow. After that optional image metadata chunks (EXIF and XMP) can follow.
@ -228,12 +230,27 @@ namespace SixLabors.ImageSharp.Formats.Webp
this.buffer[3] = 0;
uint height = (uint)BinaryPrimitives.ReadInt32LittleEndian(this.buffer) + 1;
// Optional chunks ICCP, ALPH and ANIM can follow here.
WebpChunkType chunkType = this.ReadChunkType();
while (IsOptionalVp8XChunk(chunkType))
// Read all the chunks in the order they occur.
var info = new WebpImageInfo();
while (this.currentStream.Position < this.currentStream.Length)
{
this.ParseOptionalExtendedChunks(chunkType, features);
chunkType = this.ReadChunkType();
WebpChunkType chunkType = this.ReadChunkType();
if (chunkType == WebpChunkType.Vp8)
{
info = this.ReadVp8Header(features);
}
else if (chunkType == WebpChunkType.Vp8L)
{
info = this.ReadVp8LHeader(features);
}
else if (IsOptionalVp8XChunk(chunkType))
{
this.ParseOptionalExtendedChunks(chunkType, features);
}
else
{
WebpThrowHelper.ThrowImageFormatException("Unexpected chunk followed VP8X header");
}
}
if (features.Animation)
@ -242,17 +259,7 @@ namespace SixLabors.ImageSharp.Formats.Webp
return new WebpImageInfo() { Width = width, Height = height, Features = features };
}
switch (chunkType)
{
case WebpChunkType.Vp8:
return this.ReadVp8Header(features);
case WebpChunkType.Vp8L:
return this.ReadVp8LHeader(features);
}
WebpThrowHelper.ThrowImageFormatException("Unexpected chunk followed VP8X header");
return new WebpImageInfo();
return info;
}
/// <summary>
@ -413,7 +420,7 @@ namespace SixLabors.ImageSharp.Formats.Webp
}
/// <summary>
/// Parses optional VP8X chunks, which can be ICCP, ANIM or ALPH chunks.
/// Parses optional VP8X chunks, which can be ICCP, XMP, ANIM or ALPH chunks.
/// </summary>
/// <param name="chunkType">The chunk type.</param>
/// <param name="features">The webp image features.</param>
@ -440,6 +447,38 @@ namespace SixLabors.ImageSharp.Formats.Webp
break;
case WebpChunkType.Exif:
uint exifChunkSize = this.ReadChunkSize();
if (this.IgnoreMetadata)
{
this.currentStream.Skip((int)exifChunkSize);
}
else
{
byte[] exifData = new byte[exifChunkSize];
this.currentStream.Read(exifData, 0, (int)exifChunkSize);
var profile = new ExifProfile(exifData);
this.Metadata.ExifProfile = profile;
}
break;
case WebpChunkType.Xmp:
uint xmpChunkSize = this.ReadChunkSize();
if (this.IgnoreMetadata)
{
this.currentStream.Skip((int)xmpChunkSize);
}
else
{
byte[] xmpData = new byte[xmpChunkSize];
this.currentStream.Read(xmpData, 0, (int)xmpChunkSize);
var profile = new XmpProfile(xmpData);
this.Metadata.XmpProfile = profile;
}
break;
case WebpChunkType.Animation:
// TODO: Decoding animation is not implemented yet.
break;
@ -451,6 +490,9 @@ namespace SixLabors.ImageSharp.Formats.Webp
features.AlphaData = this.memoryAllocator.Allocate<byte>(alphaDataSize);
this.currentStream.Read(features.AlphaData.Memory.Span, 0, alphaDataSize);
break;
default:
WebpThrowHelper.ThrowImageFormatException("Unexpected chunk followed VP8X header");
break;
}
}
@ -530,7 +572,9 @@ namespace SixLabors.ImageSharp.Formats.Webp
{
WebpChunkType.Alpha => true,
WebpChunkType.Animation => true,
WebpChunkType.Exif => true,
WebpChunkType.Iccp => true,
WebpChunkType.Xmp => true,
_ => false
};
}

1
src/ImageSharp/IO/BufferedReadStream.cs

@ -87,7 +87,6 @@ namespace SixLabors.ImageSharp.IO
set
{
Guard.MustBeGreaterThanOrEqualTo(value, 0, nameof(this.Position));
Guard.MustBeLessThanOrEqualTo(value, this.Length, nameof(this.Position));
// Only reset readBufferIndex if we are out of bounds of our working buffer
// otherwise we should simply move the value by the diff.

7
src/ImageSharp/Metadata/ImageFrameMetadata.cs

@ -1,12 +1,12 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Collections.Generic;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Iptc;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
namespace SixLabors.ImageSharp.Metadata
{
@ -43,8 +43,7 @@ namespace SixLabors.ImageSharp.Metadata
this.ExifProfile = other.ExifProfile?.DeepClone();
this.IccProfile = other.IccProfile?.DeepClone();
this.IptcProfile = other.IptcProfile?.DeepClone();
this.XmpProfile = other.XmpProfile != null ? new byte[other.XmpProfile.Length] : null;
other.XmpProfile?.AsSpan().CopyTo(this.XmpProfile.AsSpan());
this.XmpProfile = other.XmpProfile?.DeepClone();
}
/// <summary>
@ -55,7 +54,7 @@ namespace SixLabors.ImageSharp.Metadata
/// <summary>
/// Gets or sets the XMP profile.
/// </summary>
internal byte[] XmpProfile { get; set; }
public XmpProfile XmpProfile { get; set; }
/// <summary>
/// Gets or sets the list of ICC profiles.

10
src/ImageSharp/Metadata/ImageMetadata.cs

@ -6,6 +6,7 @@ using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Iptc;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
namespace SixLabors.ImageSharp.Metadata
{
@ -39,7 +40,7 @@ namespace SixLabors.ImageSharp.Metadata
/// <summary>
/// Initializes a new instance of the <see cref="ImageMetadata"/> class.
/// </summary>
internal ImageMetadata()
public ImageMetadata()
{
this.horizontalResolution = DefaultHorizontalResolution;
this.verticalResolution = DefaultVerticalResolution;
@ -119,13 +120,18 @@ namespace SixLabors.ImageSharp.Metadata
/// </summary>
public ExifProfile ExifProfile { get; set; }
/// <summary>
/// Gets or sets the XMP profile.
/// </summary>
public XmpProfile XmpProfile { get; set; }
/// <summary>
/// Gets or sets the list of ICC profiles.
/// </summary>
public IccProfile IccProfile { get; set; }
/// <summary>
/// Gets or sets the iptc profile.
/// Gets or sets the IPTC profile.
/// </summary>
public IptcProfile IptcProfile { get; set; }

31
src/ImageSharp/Metadata/Profiles/Exif/ExifWriter.cs

@ -46,23 +46,17 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
public byte[] GetData()
{
const uint startIndex = 0;
uint length;
IExifValue exifOffset = GetOffsetValue(this.ifdValues, this.exifValues, ExifTag.SubIFDOffset);
IExifValue gpsOffset = GetOffsetValue(this.ifdValues, this.gpsValues, ExifTag.GPSIFDOffset);
if (this.ifdValues.Count == 0 && this.exifValues.Count == 0 && this.gpsValues.Count == 0)
{
return Array.Empty<byte>();
}
uint ifdLength = this.GetLength(this.ifdValues) + 4U;
uint ifdLength = this.GetLength(this.ifdValues);
uint exifLength = this.GetLength(this.exifValues);
uint gpsLength = this.GetLength(this.gpsValues);
length = ifdLength + exifLength + gpsLength;
uint length = ifdLength + exifLength + gpsLength;
if (length == 4U)
if (length == 0)
{
return Array.Empty<byte>();
}
@ -70,9 +64,10 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
// two bytes for the byte Order marker 'II' or 'MM', followed by the number 42 (0x2A) and a 0, making 4 bytes total
length += (uint)ExifConstants.LittleEndianByteOrderMarker.Length;
length += 4 + 2;
// first IFD offset
length += 4;
var result = new byte[length];
byte[] result = new byte[length];
int i = 0;
@ -80,15 +75,13 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
ExifConstants.LittleEndianByteOrderMarker.CopyTo(result.AsSpan(start: i));
i += ExifConstants.LittleEndianByteOrderMarker.Length;
uint ifdOffset = ((uint)i - startIndex) + 4U;
uint thumbnailOffset = ifdOffset + ifdLength + exifLength + gpsLength;
uint ifdOffset = (uint)i - startIndex + 4U;
exifOffset?.TrySetValue(ifdOffset + ifdLength);
gpsOffset?.TrySetValue(ifdOffset + ifdLength + exifLength);
i = WriteUInt32(ifdOffset, result, i);
i = this.WriteHeaders(this.ifdValues, result, i);
i = WriteUInt32(thumbnailOffset, result, i);
i = this.WriteData(startIndex, this.ifdValues, result, i);
if (exifLength > 0)
@ -103,8 +96,6 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
i = this.WriteData(startIndex, this.gpsValues, result, i);
}
WriteUInt16(0, result, i);
return result;
}
@ -263,7 +254,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
{
uint valueLength = GetLength(value);
length += 2 + 2 + 4 + 4;
length += 12;
if (valueLength > 4)
{
@ -271,6 +262,9 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
}
}
// next IFD offset
length += 4;
return length;
}
@ -361,6 +355,9 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
newOffset += 4;
}
// next IFD offset
newOffset = WriteUInt32(0, destination, newOffset);
return newOffset;
}

89
src/ImageSharp/Metadata/Profiles/XMP/XmpProfile.cs

@ -0,0 +1,89 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.IO;
using System.Text;
using System.Xml.Linq;
namespace SixLabors.ImageSharp.Metadata.Profiles.Xmp
{
/// <summary>
/// Represents an XMP profile, providing access to the raw XML.
/// See <seealso href="https://www.adobe.com/devnet/xmp.html"/> for the full specification.
/// </summary>
public sealed class XmpProfile : IDeepCloneable<XmpProfile>
{
/// <summary>
/// Initializes a new instance of the <see cref="XmpProfile"/> class.
/// </summary>
public XmpProfile()
: this((byte[])null)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="XmpProfile"/> class.
/// </summary>
/// <param name="data">The UTF8 encoded byte array to read the XMP profile from.</param>
public XmpProfile(byte[] data) => this.Data = data;
/// <summary>
/// Initializes a new instance of the <see cref="XmpProfile"/> class
/// by making a copy from another XMP profile.
/// </summary>
/// <param name="other">The other XMP profile, from which the clone should be made from.</param>
private XmpProfile(XmpProfile other)
{
Guard.NotNull(other, nameof(other));
this.Data = other.Data;
}
/// <summary>
/// Gets the XMP raw data byte array.
/// </summary>
internal byte[] Data { get; private set; }
/// <summary>
/// Gets the raw XML document containing the XMP profile.
/// </summary>
/// <returns>The <see cref="XDocument"/></returns>
public XDocument GetDocument()
{
byte[] byteArray = this.Data;
if (byteArray is null)
{
return null;
}
// Strip leading whitespace, as the XmlReader doesn't like them.
int count = byteArray.Length;
for (int i = count - 1; i > 0; i--)
{
if (byteArray[i] is 0 or 0x0f)
{
count--;
}
}
using var stream = new MemoryStream(byteArray, 0, count);
using var reader = new StreamReader(stream, Encoding.UTF8);
return XDocument.Load(reader);
}
/// <summary>
/// Convert the content of this <see cref="XmpProfile"/> into a byte array.
/// </summary>
/// <returns>The <see cref="T:byte[]"/></returns>
public byte[] ToByteArray()
{
byte[] result = new byte[this.Data.Length];
this.Data.AsSpan().CopyTo(result);
return result;
}
/// <inheritdoc/>
public XmpProfile DeepClone() => new(this);
}
}

9
tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs

@ -22,7 +22,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
// TODO: A JPEGsnoop & metadata expert should review if the Exif/Icc expectations are correct.
// I'm seeing several entries with Exif-related names in images where we do not decode an exif profile. (- Anton)
public static readonly TheoryData<bool, string, int, bool, bool> MetadataTestData =
new TheoryData<bool, string, int, bool, bool>
new()
{
{ false, TestImages.Jpeg.Progressive.Progress, 24, false, false },
{ false, TestImages.Jpeg.Progressive.Fb, 24, false, true },
@ -42,15 +42,16 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
};
public static readonly TheoryData<string, int, int, PixelResolutionUnit> RatioFiles =
new TheoryData<string, int, int, PixelResolutionUnit>
new()
{
{ TestImages.Jpeg.Baseline.Ratio1x1, 1, 1, PixelResolutionUnit.AspectRatio },
{ TestImages.Jpeg.Baseline.Snake, 300, 300, PixelResolutionUnit.PixelsPerInch },
{ TestImages.Jpeg.Baseline.GammaDalaiLamaGray, 72, 72, PixelResolutionUnit.PixelsPerInch }
{ TestImages.Jpeg.Baseline.GammaDalaiLamaGray, 72, 72, PixelResolutionUnit.PixelsPerInch },
{ TestImages.Jpeg.Issues.MultipleApp01932, 400, 400, PixelResolutionUnit.PixelsPerInch }
};
public static readonly TheoryData<string, int> QualityFiles =
new TheoryData<string, int>
new()
{
{ TestImages.Jpeg.Baseline.Calliphora, 80 },
{ TestImages.Jpeg.Progressive.Fb, 75 },

13
tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs

@ -10,6 +10,7 @@ using SixLabors.ImageSharp.Formats.Tiff.Constants;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Iptc;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
using SixLabors.ImageSharp.PixelFormats;
using Xunit;
@ -132,7 +133,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff
{
Assert.NotNull(rootFrameMetaData.XmpProfile);
Assert.NotNull(rootFrameMetaData.ExifProfile);
Assert.Equal(2599, rootFrameMetaData.XmpProfile.Length);
Assert.Equal(2599, rootFrameMetaData.XmpProfile.Data.Length);
Assert.Equal(26, rootFrameMetaData.ExifProfile.Values.Count);
}
}
@ -163,7 +164,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff
Assert.Equal(32, rootFrame.Width);
Assert.Equal(32, rootFrame.Height);
Assert.NotNull(rootFrame.Metadata.XmpProfile);
Assert.Equal(2599, rootFrame.Metadata.XmpProfile.Length);
Assert.Equal(2599, rootFrame.Metadata.XmpProfile.Data.Length);
ExifProfile exifProfile = rootFrame.Metadata.ExifProfile;
TiffFrameMetadata tiffFrameMetadata = rootFrame.Metadata.GetTiffMetadata();
@ -251,7 +252,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff
ImageMetadata inputMetaData = image.Metadata;
ImageFrame<TPixel> rootFrameInput = image.Frames.RootFrame;
TiffFrameMetadata frameMetaInput = rootFrameInput.Metadata.GetTiffMetadata();
byte[] xmpProfileInput = rootFrameInput.Metadata.XmpProfile;
XmpProfile xmpProfileInput = rootFrameInput.Metadata.XmpProfile;
ExifProfile exifProfileInput = rootFrameInput.Metadata.ExifProfile;
Assert.Equal(TiffCompression.Lzw, frameMetaInput.Compression);
@ -270,7 +271,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff
ImageFrame<Rgba32> rootFrameEncodedImage = encodedImage.Frames.RootFrame;
TiffFrameMetadata tiffMetaDataEncodedRootFrame = rootFrameEncodedImage.Metadata.GetTiffMetadata();
ExifProfile encodedImageExifProfile = rootFrameEncodedImage.Metadata.ExifProfile;
byte[] encodedImageXmpProfile = rootFrameEncodedImage.Metadata.XmpProfile;
XmpProfile encodedImageXmpProfile = rootFrameEncodedImage.Metadata.XmpProfile;
Assert.Equal(TiffBitsPerPixel.Bit4, tiffMetaDataEncodedRootFrame.BitsPerPixel);
Assert.Equal(TiffCompression.Lzw, tiffMetaDataEncodedRootFrame.Compression);
@ -288,7 +289,9 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff
Assert.Equal(exifProfileInput.GetValue(ExifTag.XResolution).Value.ToDouble(), encodedImageExifProfile.GetValue(ExifTag.XResolution).Value.ToDouble());
Assert.Equal(exifProfileInput.GetValue(ExifTag.YResolution).Value.ToDouble(), encodedImageExifProfile.GetValue(ExifTag.YResolution).Value.ToDouble());
Assert.Equal(xmpProfileInput, encodedImageXmpProfile);
Assert.NotNull(xmpProfileInput);
Assert.NotNull(encodedImageXmpProfile);
Assert.Equal(xmpProfileInput.Data, encodedImageXmpProfile.Data);
Assert.Equal("IrfanView", exifProfileInput.GetValue(ExifTag.Software).Value);
Assert.Equal("This is Название", exifProfileInput.GetValue(ExifTag.ImageDescription).Value);

20
tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs

@ -63,6 +63,26 @@ namespace SixLabors.ImageSharp.Tests.Formats.Webp
}
}
[Theory]
[WithFile(TestImages.Webp.Lossy.WithXmp, PixelTypes.Rgba32, false)]
[WithFile(TestImages.Webp.Lossy.WithXmp, PixelTypes.Rgba32, true)]
public async void IgnoreMetadata_ControlsWhetherXmpIsParsed<TPixel>(TestImageProvider<TPixel> provider, bool ignoreMetadata)
where TPixel : unmanaged, IPixel<TPixel>
{
var decoder = new WebpDecoder { IgnoreMetadata = ignoreMetadata };
using Image<TPixel> image = await provider.GetImageAsync(decoder);
if (ignoreMetadata)
{
Assert.Null(image.Metadata.XmpProfile);
}
else
{
Assert.NotNull(image.Metadata.XmpProfile);
Assert.NotEmpty(image.Metadata.XmpProfile.Data);
}
}
[Theory]
[InlineData(WebpFileFormatType.Lossy)]
[InlineData(WebpFileFormatType.Lossless)]

24
tests/ImageSharp.Tests/IO/BufferedReadStreamTests.cs

@ -314,7 +314,7 @@ namespace SixLabors.ImageSharp.Tests.IO
[Theory]
[MemberData(nameof(BufferSizes))]
public void BufferedStreamThrowsOnBadPosition(int bufferSize)
public void BufferedStreamThrowsOnNegativePosition(int bufferSize)
{
this.configuration.StreamProcessingBufferSize = bufferSize;
using (MemoryStream stream = this.CreateTestStream(bufferSize))
@ -322,15 +322,14 @@ namespace SixLabors.ImageSharp.Tests.IO
using (var reader = new BufferedReadStream(this.configuration, stream))
{
Assert.Throws<ArgumentOutOfRangeException>(() => reader.Position = -stream.Length);
Assert.Throws<ArgumentOutOfRangeException>(() => reader.Position = stream.Length + 1);
}
}
}
[Fact]
public void BufferedStreamCanSetPositionToEnd()
[Theory]
[MemberData(nameof(BufferSizes))]
public void BufferedStreamCanSetPositionToEnd(int bufferSize)
{
var bufferSize = 8;
this.configuration.StreamProcessingBufferSize = bufferSize;
using (MemoryStream stream = this.CreateTestStream(bufferSize * 2))
{
@ -341,6 +340,21 @@ namespace SixLabors.ImageSharp.Tests.IO
}
}
[Theory]
[MemberData(nameof(BufferSizes))]
public void BufferedStreamCanSetPositionPastTheEnd(int bufferSize)
{
this.configuration.StreamProcessingBufferSize = bufferSize;
using (MemoryStream stream = this.CreateTestStream(bufferSize * 2))
{
using (var reader = new BufferedReadStream(this.configuration, stream))
{
reader.Position = reader.Length + 1;
Assert.Equal(stream.Length + 1, stream.Position);
}
}
}
private MemoryStream CreateTestStream(int length)
{
var buffer = new byte[length];

8
tests/ImageSharp.Tests/Metadata/ImageFrameMetadataTests.cs

@ -1,10 +1,10 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System.Linq;
using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
using Xunit;
using ExifProfile = SixLabors.ImageSharp.Metadata.Profiles.Exif.ExifProfile;
using ExifTag = SixLabors.ImageSharp.Metadata.Profiles.Exif.ExifTag;
@ -41,10 +41,10 @@ namespace SixLabors.ImageSharp.Tests.Metadata
public void CloneIsDeep()
{
// arrange
byte[] xmpProfile = { 1, 2, 3 };
var exifProfile = new ExifProfile();
exifProfile.SetValue(ExifTag.Software, "UnitTest");
exifProfile.SetValue(ExifTag.Artist, "UnitTest");
var xmpProfile = new XmpProfile(new byte[0]);
var iccProfile = new IccProfile()
{
Header = new IccProfileHeader()
@ -72,8 +72,8 @@ namespace SixLabors.ImageSharp.Tests.Metadata
Assert.NotNull(clone.IptcProfile);
Assert.False(metaData.ExifProfile.Equals(clone.ExifProfile));
Assert.True(metaData.ExifProfile.Values.Count == clone.ExifProfile.Values.Count);
Assert.False(metaData.XmpProfile.Equals(clone.XmpProfile));
Assert.True(metaData.XmpProfile.SequenceEqual(clone.XmpProfile));
Assert.False(ReferenceEquals(metaData.XmpProfile, clone.XmpProfile));
Assert.True(metaData.XmpProfile.Data.Equals(clone.XmpProfile.Data));
Assert.False(metaData.GetGifMetadata().Equals(clone.GetGifMetadata()));
Assert.False(metaData.IccProfile.Equals(clone.IccProfile));
Assert.False(metaData.IptcProfile.Equals(clone.IptcProfile));

31
tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@ -86,6 +87,18 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Exif
new ExifProfile(Array.Empty<byte>());
}
[Fact]
public void EmptyWriter()
{
var profile = new ExifProfile() { Parts = ExifParts.GpsTags };
profile.SetValue(ExifTag.Copyright, "Copyright text");
byte[] bytes = profile.ToByteArray();
Assert.NotNull(bytes);
Assert.Empty(bytes);
}
[Fact]
public void ConstructorCopy()
{
@ -420,7 +433,7 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Exif
Assert.Equal(2, profile.Values.Count(v => (ExifTagValue)(ushort)v.Tag == ExifTagValue.DateTime));
byte[] bytes = profile.ToByteArray();
Assert.Equal(525, bytes.Length);
Assert.Equal(531, bytes.Length);
var profile2 = new ExifProfile(bytes);
Assert.Equal(25, profile2.Values.Count);
@ -487,6 +500,22 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Exif
return profile;
}
[Fact]
public void IfdStructure()
{
var exif = new ExifProfile();
exif.SetValue(ExifTag.XPAuthor, Encoding.GetEncoding("UCS-2").GetBytes("Dan Petitt"));
Span<byte> actualBytes = exif.ToByteArray();
// Assert
int ifdOffset = ExifConstants.LittleEndianByteOrderMarker.Length;
Assert.Equal(8U, BinaryPrimitives.ReadUInt32LittleEndian(actualBytes.Slice(ifdOffset, 4)));
int nextIfdPointerOffset = ExifConstants.LittleEndianByteOrderMarker.Length + 4 + 2 + 12;
Assert.Equal(0U, BinaryPrimitives.ReadUInt32LittleEndian(actualBytes.Slice(nextIfdPointerOffset, 4)));
}
internal static ExifProfile GetExifProfile()
{
using Image<Rgba32> image = TestFile.Create(TestImages.Jpeg.Baseline.Floorplan).CreateRgba32Image();

260
tests/ImageSharp.Tests/Metadata/Profiles/XMP/XmpProfileTests.cs

@ -0,0 +1,260 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.IO;
using System.Text;
using System.Xml.Linq;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Formats.Tiff;
using SixLabors.ImageSharp.Formats.Webp;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
using SixLabors.ImageSharp.PixelFormats;
using Xunit;
namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Xmp
{
public class XmpProfileTests
{
private static GifDecoder GifDecoder => new() { IgnoreMetadata = false };
private static JpegDecoder JpegDecoder => new() { IgnoreMetadata = false };
private static PngDecoder PngDecoder => new() { IgnoreMetadata = false };
private static TiffDecoder TiffDecoder => new() { IgnoreMetadata = false };
private static WebpDecoder WebpDecoder => new() { IgnoreMetadata = false };
[Theory]
[WithFile(TestImages.Gif.Receipt, PixelTypes.Rgba32)]
public async void ReadXmpMetadata_FromGif_Works<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using (Image<TPixel> image = await provider.GetImageAsync(GifDecoder))
{
XmpProfile actual = image.Metadata.XmpProfile ?? image.Frames.RootFrame.Metadata.XmpProfile;
XmpProfileContainsExpectedValues(actual);
}
}
[Theory]
[WithFile(TestImages.Jpeg.Baseline.Lake, PixelTypes.Rgba32)]
[WithFile(TestImages.Jpeg.Baseline.Metadata, PixelTypes.Rgba32)]
[WithFile(TestImages.Jpeg.Baseline.ExtendedXmp, PixelTypes.Rgba32)]
public async void ReadXmpMetadata_FromJpg_Works<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using (Image<TPixel> image = await provider.GetImageAsync(JpegDecoder))
{
XmpProfile actual = image.Metadata.XmpProfile ?? image.Frames.RootFrame.Metadata.XmpProfile;
XmpProfileContainsExpectedValues(actual);
}
}
[Theory]
[WithFile(TestImages.Png.XmpColorPalette, PixelTypes.Rgba32)]
public async void ReadXmpMetadata_FromPng_Works<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using (Image<TPixel> image = await provider.GetImageAsync(PngDecoder))
{
XmpProfile actual = image.Metadata.XmpProfile ?? image.Frames.RootFrame.Metadata.XmpProfile;
XmpProfileContainsExpectedValues(actual);
}
}
[Theory]
[WithFile(TestImages.Tiff.SampleMetadata, PixelTypes.Rgba32)]
public async void ReadXmpMetadata_FromTiff_Works<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using (Image<TPixel> image = await provider.GetImageAsync(TiffDecoder))
{
XmpProfile actual = image.Metadata.XmpProfile ?? image.Frames.RootFrame.Metadata.XmpProfile;
XmpProfileContainsExpectedValues(actual);
}
}
[Theory]
[WithFile(TestImages.Webp.Lossy.WithXmp, PixelTypes.Rgba32)]
public async void ReadXmpMetadata_FromWebp_Works<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using (Image<TPixel> image = await provider.GetImageAsync(WebpDecoder))
{
XmpProfile actual = image.Metadata.XmpProfile ?? image.Frames.RootFrame.Metadata.XmpProfile;
XmpProfileContainsExpectedValues(actual);
}
}
[Fact]
public void XmpProfile_ToFromByteArray_ReturnsClone()
{
// arrange
XmpProfile profile = CreateMinimalXmlProfile();
byte[] original = profile.ToByteArray();
// act
byte[] actual = profile.ToByteArray();
// assert
Assert.False(ReferenceEquals(original, actual));
}
[Fact]
public void XmpProfile_CloneIsDeep()
{
// arrange
XmpProfile profile = CreateMinimalXmlProfile();
byte[] original = profile.ToByteArray();
// act
XmpProfile clone = profile.DeepClone();
byte[] actual = clone.ToByteArray();
// assert
Assert.False(ReferenceEquals(original, actual));
}
[Fact]
public void WritingGif_PreservesXmpProfile()
{
// arrange
var image = new Image<Rgba32>(1, 1);
XmpProfile original = CreateMinimalXmlProfile();
image.Metadata.XmpProfile = original;
var encoder = new GifEncoder();
// act
using Image<Rgba32> reloadedImage = WriteAndRead(image, encoder);
// assert
XmpProfile actual = reloadedImage.Metadata.XmpProfile ?? reloadedImage.Frames.RootFrame.Metadata.XmpProfile;
XmpProfileContainsExpectedValues(actual);
Assert.Equal(original.Data, actual.Data);
}
[Fact]
public void WritingJpeg_PreservesXmpProfile()
{
// arrange
var image = new Image<Rgba32>(1, 1);
XmpProfile original = CreateMinimalXmlProfile();
image.Metadata.XmpProfile = original;
var encoder = new JpegEncoder();
// act
using Image<Rgba32> reloadedImage = WriteAndRead(image, encoder);
// assert
XmpProfile actual = reloadedImage.Metadata.XmpProfile ?? reloadedImage.Frames.RootFrame.Metadata.XmpProfile;
XmpProfileContainsExpectedValues(actual);
Assert.Equal(original.Data, actual.Data);
}
[Fact]
public async void WritingJpeg_PreservesExtendedXmpProfile()
{
// arrange
var provider = TestImageProvider<Rgba32>.File(TestImages.Jpeg.Baseline.ExtendedXmp);
using Image<Rgba32> image = await provider.GetImageAsync(JpegDecoder);
XmpProfile original = image.Metadata.XmpProfile;
var encoder = new JpegEncoder();
// act
using Image<Rgba32> reloadedImage = WriteAndRead(image, encoder);
// assert
XmpProfile actual = reloadedImage.Metadata.XmpProfile ?? reloadedImage.Frames.RootFrame.Metadata.XmpProfile;
XmpProfileContainsExpectedValues(actual);
Assert.Equal(original.Data, actual.Data);
}
[Fact]
public void WritingPng_PreservesXmpProfile()
{
// arrange
var image = new Image<Rgba32>(1, 1);
XmpProfile original = CreateMinimalXmlProfile();
image.Metadata.XmpProfile = original;
var encoder = new PngEncoder();
// act
using Image<Rgba32> reloadedImage = WriteAndRead(image, encoder);
// assert
XmpProfile actual = reloadedImage.Metadata.XmpProfile ?? reloadedImage.Frames.RootFrame.Metadata.XmpProfile;
XmpProfileContainsExpectedValues(actual);
Assert.Equal(original.Data, actual.Data);
}
[Fact]
public void WritingTiff_PreservesXmpProfile()
{
// arrange
var image = new Image<Rgba32>(1, 1);
XmpProfile original = CreateMinimalXmlProfile();
image.Frames.RootFrame.Metadata.XmpProfile = original;
var encoder = new TiffEncoder();
// act
using Image<Rgba32> reloadedImage = WriteAndRead(image, encoder);
// assert
XmpProfile actual = reloadedImage.Metadata.XmpProfile ?? reloadedImage.Frames.RootFrame.Metadata.XmpProfile;
XmpProfileContainsExpectedValues(actual);
Assert.Equal(original.Data, actual.Data);
}
[Fact]
public void WritingWebp_PreservesXmpProfile()
{
// arrange
var image = new Image<Rgba32>(1, 1);
XmpProfile original = CreateMinimalXmlProfile();
image.Metadata.XmpProfile = original;
var encoder = new WebpEncoder();
// act
using Image<Rgba32> reloadedImage = WriteAndRead(image, encoder);
// assert
XmpProfile actual = reloadedImage.Metadata.XmpProfile ?? reloadedImage.Frames.RootFrame.Metadata.XmpProfile;
XmpProfileContainsExpectedValues(actual);
Assert.Equal(original.Data, actual.Data);
}
private static void XmpProfileContainsExpectedValues(XmpProfile xmp)
{
Assert.NotNull(xmp);
XDocument document = xmp.GetDocument();
Assert.NotNull(document);
Assert.Equal("xmpmeta", document.Root.Name.LocalName);
Assert.Equal("adobe:ns:meta/", document.Root.Name.NamespaceName);
}
private static XmpProfile CreateMinimalXmlProfile()
{
string content = $"<?xpacket begin='' id='{Guid.NewGuid()}'?><x:xmpmeta xmlns:x='adobe:ns:meta/'></x:xmpmeta><?xpacket end='w'?> ";
byte[] data = Encoding.UTF8.GetBytes(content);
var profile = new XmpProfile(data);
return profile;
}
private static Image<Rgba32> WriteAndRead(Image<Rgba32> image, IImageEncoder encoder)
{
using (var memStream = new MemoryStream())
{
image.Save(memStream, encoder);
image.Dispose();
memStream.Position = 0;
return Image.Load<Rgba32>(memStream);
}
}
}
}

5
tests/ImageSharp.Tests/TestImages.cs

@ -61,6 +61,7 @@ namespace SixLabors.ImageSharp.Tests
public const string David = "Png/david.png";
public const string TestPattern31x31 = "Png/testpattern31x31.png";
public const string TestPattern31x31HalfTransparent = "Png/testpattern31x31-halftransparent.png";
public const string XmpColorPalette = "Png/xmp-colorpalette.png";
// Filtered test images from http://www.schaik.com/pngsuite/pngsuite_fil_png.html
public const string Filter0 = "Png/filter0.png";
@ -218,6 +219,8 @@ namespace SixLabors.ImageSharp.Tests
public const string ArithmeticCodingProgressive = "Jpg/progressive/arithmetic_progressive.jpg";
public const string Lossless = "Jpg/baseline/lossless.jpg";
public const string Winter444_Interleaved = "Jpg/baseline/winter444_interleaved.jpg";
public const string Metadata = "Jpg/baseline/Metadata-test-file.jpg";
public const string ExtendedXmp = "Jpg/baseline/extended-xmp.jpg";
public static readonly string[] All =
{
@ -257,6 +260,7 @@ namespace SixLabors.ImageSharp.Tests
public const string IdentifyMultiFrame1211 = "Jpg/issues/issue-1221-identify-multi-frame.jpg";
public const string WrongColorSpace = "Jpg/issues/Issue1732-WrongColorSpace.jpg";
public const string MalformedUnsupportedComponentCount = "Jpg/issues/issue-1900-malformed-unsupported-255-components.jpg";
public const string MultipleApp01932 = "Jpg/issues/issue-1932-app0-resolution.jpg";
public static class Fuzz
{
@ -625,6 +629,7 @@ namespace SixLabors.ImageSharp.Tests
public const string Earth = "Webp/earth_lossy.webp";
public const string WithExif = "Webp/exif_lossy.webp";
public const string WithIccp = "Webp/lossy_with_iccp.webp";
public const string WithXmp = "Webp/xmp_lossy.webp";
public const string BikeSmall = "Webp/bike_lossless_small.webp";
// Lossy images without macroblock filtering.

3
tests/Images/Input/Jpg/baseline/Metadata-test-file.jpg

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:800911efb6f0c796d61b5ea14fc67fe891aaae3c04a49cfd5b86e68958598436
size 138810

3
tests/Images/Input/Jpg/baseline/extended-xmp.jpg

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:000c67f210059b101570949e889846bb11d6bdc801bd641d7d26424ad9cd027f
size 623986

3
tests/Images/Input/Jpg/issues/issue-1932-app0-resolution.jpg

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3654c48003b85c1110bad8c31d2f94eaf4dcfe488698246b3ead4b54715d8d18
size 1325

3
tests/Images/Input/Png/xmp-colorpalette.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:fb55607fd7de6a47d8dd242c1a7be9627c564821554db896ed46603d15963c06
size 1025

3
tests/Images/Input/Webp/xmp_lossy.webp

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:755a63652695d7e190f375c9c0697cd37c9b601cd54405c704ec8efc200e67fc
size 474772
Loading…
Cancel
Save