diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml
index 5189f0435..6d31e8c53 100644
--- a/.github/workflows/build-and-test.yml
+++ b/.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
diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml
new file mode 100644
index 000000000..2b14f2a4b
--- /dev/null
+++ b/.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
diff --git a/ImageSharp.sln b/ImageSharp.sln
index f16f98ac5..17d293b43 100644
--- a/ImageSharp.sln
+++ b/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}"
diff --git a/src/ImageSharp/Formats/Gif/GifConstants.cs b/src/ImageSharp/Formats/Gif/GifConstants.cs
index 24fd8a936..1179b67b1 100644
--- a/src/ImageSharp/Formats/Gif/GifConstants.cs
+++ b/src/ImageSharp/Formats/Gif/GifConstants.cs
@@ -121,5 +121,16 @@ namespace SixLabors.ImageSharp.Formats.Gif
(byte)'P', (byte)'E',
(byte)'2', (byte)'.', (byte)'0'
};
+
+ ///
+ /// Gets the ASCII encoded application identification bytes.
+ ///
+ internal static ReadOnlySpan XmpApplicationIdentificationBytes => new[]
+ {
+ (byte)'X', (byte)'M', (byte)'P',
+ (byte)' ', (byte)'D', (byte)'a',
+ (byte)'t', (byte)'a',
+ (byte)'X', (byte)'M', (byte)'P'
+ };
}
}
diff --git a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs
index 3e33a6e37..b6348803a 100644
--- a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs
+++ b/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
}
///
- /// Reads the application extension block parsing any animation information
+ /// Reads the application extension block parsing any animation or XMP information
/// if present.
///
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;
}
diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs
index 05ea14e9c..a21b050a8 100644
--- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs
+++ b/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.
///
/// The stream to write to.
+ /// The frame count fo this image.
/// The animated image repeat count.
- private void WriteApplicationExtension(Stream stream, ushort repeatCount)
+ /// The XMP metadata profile. Null if profile is not to be written.
+ 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);
+ }
}
///
@@ -420,14 +427,28 @@ namespace SixLabors.ImageSharp.Formats.Gif
private void WriteExtension(TGifExtension extension, Stream stream)
where TGifExtension : struct, IGifExtension
{
- this.buffer[0] = GifConstants.ExtensionIntroducer;
- this.buffer[1] = extension.Label;
+ IMemoryOwner owner = null;
+ Span buffer;
+ int extensionSize = extension.ContentLength;
+ if (extensionSize > this.buffer.Length - 3)
+ {
+ owner = this.memoryAllocator.Allocate(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();
}
///
diff --git a/src/ImageSharp/Formats/Gif/Sections/GifGraphicControlExtension.cs b/src/ImageSharp/Formats/Gif/Sections/GifGraphicControlExtension.cs
index ee5a43d80..801849c9b 100644
--- a/src/ImageSharp/Formats/Gif/Sections/GifGraphicControlExtension.cs
+++ b/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 buffer)
{
ref GifGraphicControlExtension dest = ref Unsafe.As(ref MemoryMarshal.GetReference(buffer));
diff --git a/src/ImageSharp/Formats/Gif/Sections/GifNetscapeLoopingApplicationExtension.cs b/src/ImageSharp/Formats/Gif/Sections/GifNetscapeLoopingApplicationExtension.cs
index 26faa8925..2c7bed611 100644
--- a/src/ImageSharp/Formats/Gif/Sections/GifNetscapeLoopingApplicationExtension.cs
+++ b/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;
+
///
/// Gets the repeat count.
/// 0 means loop indefinitely. Count is set as play n + 1 times.
diff --git a/src/ImageSharp/Formats/Gif/Sections/GifXmpApplicationExtension.cs b/src/ImageSharp/Formats/Gif/Sections/GifXmpApplicationExtension.cs
new file mode 100644
index 000000000..236508fe9
--- /dev/null
+++ b/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
+
+ ///
+ /// Gets the raw Data.
+ ///
+ public byte[] Data { get; }
+
+ ///
+ /// Reads the XMP metadata from the specified stream.
+ ///
+ /// The stream to read from.
+ /// The XMP metadata
+ /// Thrown if the XMP block is not properly terminated.
+ 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();
+ 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 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 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 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 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;
+ }
+ }
+}
diff --git a/src/ImageSharp/Formats/Gif/Sections/IGifExtension.cs b/src/ImageSharp/Formats/Gif/Sections/IGifExtension.cs
index 5a15a6dfa..d2783fc48 100644
--- a/src/ImageSharp/Formats/Gif/Sections/IGifExtension.cs
+++ b/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
///
byte Label { get; }
+ ///
+ /// Gets the length of the contents of this extension.
+ ///
+ int ContentLength { get; }
+
///
/// Writes the extension data to the buffer.
///
diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs
index ce5e5110b..2ae3ae86b 100644
--- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs
+++ b/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
/// Table index.
/// Code lengths.
/// Code values.
+ /// The provided spare workspace memory, can be dirty.
[MethodImpl(InliningOptions.ShortMethod)]
- public void BuildHuffmanTable(int type, int index, ReadOnlySpan codeLengths, ReadOnlySpan values)
+ public void BuildHuffmanTable(int type, int index, ReadOnlySpan codeLengths, ReadOnlySpan values, Span workspace)
{
HuffmanTable[] tables = type == 0 ? this.dcHuffmanTables : this.acHuffmanTables;
- tables[index] = new HuffmanTable(codeLengths, values);
+ tables[index] = new HuffmanTable(codeLengths, values, workspace);
}
}
}
diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs
index f18c63627..bee5e0229 100644
--- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs
+++ b/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;
-
///
- /// 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 ctor.
///
- public fixed byte Sizes[17];
+ public const int WorkspaceByteSize = 256 * sizeof(uint);
///
/// 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
///
/// Initializes a new instance of the struct.
///
- /// The code lengths
- /// The huffman values
- public HuffmanTable(ReadOnlySpan codeLengths, ReadOnlySpan values)
+ /// The code lengths.
+ /// The huffman values.
+ /// The provided spare workspace memory, can be dirty.
+ public HuffmanTable(ReadOnlySpan codeLengths, ReadOnlySpan values, Span 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);
- }
-
- ///
- /// Expands the HuffmanTable into its readable form.
- ///
- public void Configure()
- {
- if (this.isConfigured)
- {
- return;
- }
- Span huffSize = stackalloc char[257];
- Span 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;
}
}
}
diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JFifMarker.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JFifMarker.cs
index c7b71f75a..a95e6c16c 100644
--- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JFifMarker.cs
+++ b/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
///
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;
- }
///
public override bool Equals(object obj) => obj is JFifMarker other && this.Equals(other);
///
public override int GetHashCode()
- {
- return HashCode.Combine(
+ => HashCode.Combine(
this.MajorVersion,
this.MinorVersion,
this.DensityUnits,
this.XDensity,
this.YDensity);
- }
}
}
diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ProfileResolver.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ProfileResolver.cs
index e1e0e160c..b41c949b2 100644
--- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ProfileResolver.cs
+++ b/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'
};
+ ///
+ /// Gets the XMP specific markers.
+ ///
+ public static ReadOnlySpan 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
+ };
+
///
/// Gets the Adobe specific markers .
///
diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
index 73af42afd..023928f37 100644
--- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
+++ b/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
///
/// Whether the image has an EXIF marker.
///
- private bool isExif;
+ private bool hasExif;
///
/// Contains exif data.
@@ -56,7 +57,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
///
/// Whether the image has an ICC marker.
///
- private bool isIcc;
+ private bool hasIcc;
///
/// Contains ICC data.
@@ -66,13 +67,23 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
///
/// Whether the image has a IPTC data.
///
- private bool isIptc;
+ private bool hasIptc;
///
/// Contains IPTC data.
///
private byte[] iptcData;
+ ///
+ /// Whether the image has a XMP data.
+ ///
+ private bool hasXmp;
+
+ ///
+ /// Contains XMP data.
+ ///
+ private byte[] xmpData;
+
///
/// Contains information about the JFIF marker.
///
@@ -183,6 +194,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
this.InitExifProfile();
this.InitIccProfile();
this.InitIptcProfile();
+ this.InitXmpProfile();
this.InitDerivedMetadataProperties();
return new Image(
@@ -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
///
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
///
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
///
private void InitIptcProfile()
{
- if (this.isIptc)
+ if (this.hasIptc)
{
var profile = new IptcProfile(this.iptcData);
this.Metadata.IptcProfile = profile;
}
}
+ ///
+ /// Initializes the XMP profile.
+ ///
+ private void InitXmpProfile()
+ {
+ if (this.hasXmp)
+ {
+ var profile = new XmpProfile(this.xmpData);
+ this.Metadata.XmpProfile = profile;
+ }
+ }
+
///
/// Assigns derived metadata properties to , eg. horizontal and vertical resolution if it has a JFIF header.
///
@@ -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
/// The remaining bytes in the segment block.
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);
}
///
@@ -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);
+ }
}
///
@@ -1095,12 +1160,18 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// The remaining bytes in the segment block.
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 huffmanData = this.Configuration.MemoryAllocator.Allocate(256, AllocationOptions.Clean))
+ int length = remaining;
+ using (IMemoryOwner buffer = this.Configuration.MemoryAllocator.Allocate(totalBufferSize))
{
- Span huffmanDataSpan = huffmanData.GetSpan();
- ref byte huffmanDataRef = ref MemoryMarshal.GetReference(huffmanDataSpan);
+ Span bufferSpan = buffer.GetSpan();
+ Span huffmanLegthsSpan = bufferSpan.Slice(0, codeLengthsByteSize);
+ Span huffmanValuesSpan = bufferSpan.Slice(codeLengthsByteSize, codeValuesMaxByteSize);
+ Span tableWorkspace = MemoryMarshal.Cast(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 codeLengths = this.Configuration.MemoryAllocator.Allocate(17, AllocationOptions.Clean))
+ int codeLengthSum = 0;
+ for (int j = 1; j < 17; j++)
{
- Span 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 huffmanValues = this.Configuration.MemoryAllocator.Allocate(256, AllocationOptions.Clean))
- {
- Span 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);
}
}
}
diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs
index abe59516f..a3cff8f31 100644
--- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs
+++ b/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);
}
+ ///
+ /// Writes the XMP metadata.
+ ///
+ /// The XMP metadata to write.
+ ///
+ /// Thrown if the XMP profile size exceeds the limit of 65533 bytes.
+ ///
+ 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;
+ }
+ }
+
///
/// Writes the App1 header.
///
@@ -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);
}
diff --git a/src/ImageSharp/Formats/PixelTypeInfo.cs b/src/ImageSharp/Formats/PixelTypeInfo.cs
index 718b05e33..afa3b19a2 100644
--- a/src/ImageSharp/Formats/PixelTypeInfo.cs
+++ b/src/ImageSharp/Formats/PixelTypeInfo.cs
@@ -15,8 +15,17 @@ namespace SixLabors.ImageSharp.Formats
/// Initializes a new instance of the class.
///
/// Color depth, in number of bits per pixel.
- /// Tthe pixel alpha transparency behavior.
- internal PixelTypeInfo(int bitsPerPixel, PixelAlphaRepresentation? alpha = null)
+ public PixelTypeInfo(int bitsPerPixel)
+ {
+ this.BitsPerPixel = bitsPerPixel;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Color depth, in number of bits per pixel.
+ /// The pixel alpha transparency behavior.
+ public PixelTypeInfo(int bitsPerPixel, PixelAlphaRepresentation alpha)
{
this.BitsPerPixel = bitsPerPixel;
this.AlphaRepresentation = alpha;
diff --git a/src/ImageSharp/Formats/Png/PngConstants.cs b/src/ImageSharp/Formats/Png/PngConstants.cs
index b4ef28083..fcc8fd992 100644
--- a/src/ImageSharp/Formats/Png/PngConstants.cs
+++ b/src/ImageSharp/Formats/Png/PngConstants.cs
@@ -78,5 +78,29 @@ namespace SixLabors.ImageSharp.Formats.Png
0x1A, // EOF
0x0A // LF
};
+
+ ///
+ /// Gets the keyword of the XMP metadata, encoded in an iTXT chunk.
+ ///
+ public static ReadOnlySpan 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'
+ };
}
}
diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs
index c9f0ce375..f5fc86ee4 100644
--- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs
+++ b/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
///
/// The metadata to decode to.
/// The containing the data.
- private void ReadInternationalTextChunk(PngMetadata metadata, ReadOnlySpan data)
+ private void ReadInternationalTextChunk(ImageMetadata metadata, ReadOnlySpan 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 keywordBytes) => keywordBytes.SequenceEqual(PngConstants.XmpKeyword);
+
private void SwapScanlineBuffers()
{
IMemoryOwner temp = this.previousScanline;
diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs
index 5e067aba5..c443c0fcf 100644
--- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs
+++ b/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());
}
+ ///
+ /// Writes an iTXT chunk, containing the XMP metdata to the stream, if such profile is present in the metadata.
+ ///
+ /// The containing image data.
+ /// The image metadata.
+ 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 owner = this.memoryAllocator.Allocate(payloadLength))
+ {
+ Span 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);
+ }
+ }
+
///
/// 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 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 owner = this.memoryAllocator.Allocate(payloadLength))
{
- // Indicate that the text is compressed.
- outputBytes[keywordBytes.Length + 1] = 1;
- }
+ Span 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 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 owner = this.memoryAllocator.Allocate(payloadLength))
+ {
+ Span 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 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 owner = this.memoryAllocator.Allocate(payloadLength))
+ {
+ Span 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());
+ }
}
}
}
diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
index 177a93d24..05c5358f5 100644
--- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
+++ b/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 DecodeFrame(ExifProfile tags, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
- 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);
diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderMetadataCreator.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderMetadataCreator.cs
index 4c4023ace..ddbfbcb48 100644
--- a/src/ImageSharp/Formats/Tiff/TiffDecoderMetadataCreator.cs
+++ b/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 xmpProfileBytes = frameMetaData.ExifProfile.GetValue(ExifTag.XMP);
+ if (xmpProfileBytes != null)
+ {
+ frameMetaData.XmpProfile = new XmpProfile(xmpProfileBytes.Value);
+ }
+
IExifValue iccProfileBytes = frameMetaData.ExifProfile.GetValue(ExifTag.IccProfile);
if (iccProfileBytes != null)
{
diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs
index 55dd7d397..e54d029ab 100644
--- a/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs
+++ b/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);
diff --git a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs
index 920888136..ac039be79 100644
--- a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs
+++ b/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
}
///
- /// Calculates the exif chunk size.
+ /// Calculates the chunk size of EXIF or XMP metadata.
///
- /// The exif profile bytes.
+ /// The metadata profile bytes.
/// The exif chunk size in bytes.
- 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;
}
///
- /// Writes the Exif profile to the stream.
+ /// Writes a metadata profile (EXIF or XMP) to the stream.
///
/// The stream to write to.
- /// The exif profile bytes.
- protected void WriteExifProfile(Stream stream, byte[] exifBytes)
+ /// The metadata profile's bytes.
+ /// The chuck type to write.
+ 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 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
///
/// The stream to write to.
/// A exif profile or null, if it does not exist.
+ /// A XMP profile or null, if it does not exist.
/// The width of the image.
/// The height of the image.
/// Flag indicating, if a alpha channel is present.
- 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.
diff --git a/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs b/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs
index 3b2f943db..4e91bedb0 100644
--- a/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs
+++ b/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
///
/// The stream to write to.
/// The exif profile.
+ /// The XMP profile.
/// The width of the image.
/// The height of the image.
/// Flag indicating, if a alpha channel is present.
- 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);
diff --git a/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs b/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs
index b83865aa3..d41224f90 100644
--- a/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs
+++ b/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
///
/// The stream to write to.
/// The exif profile.
+ /// The XMP profile.
/// The width of the image.
/// The height of the image.
/// Flag indicating, if a alpha channel is present.
- 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);
}
}
diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs
index 8566566f6..e9dce913a 100644
--- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs
+++ b/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);
}
///
diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs
index 37e09d080..022232050 100644
--- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs
+++ b/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);
}
///
diff --git a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs
index 09071406c..9d18e5d82 100644
--- a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs
+++ b/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;
}
///
@@ -413,7 +420,7 @@ namespace SixLabors.ImageSharp.Formats.Webp
}
///
- /// 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.
///
/// The chunk type.
/// The webp image features.
@@ -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(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
};
}
diff --git a/src/ImageSharp/IO/BufferedReadStream.cs b/src/ImageSharp/IO/BufferedReadStream.cs
index acba3eff0..4ab7f312b 100644
--- a/src/ImageSharp/IO/BufferedReadStream.cs
+++ b/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.
diff --git a/src/ImageSharp/Metadata/ImageFrameMetadata.cs b/src/ImageSharp/Metadata/ImageFrameMetadata.cs
index 1819fd2bc..1cad4ebe8 100644
--- a/src/ImageSharp/Metadata/ImageFrameMetadata.cs
+++ b/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();
}
///
@@ -55,7 +54,7 @@ namespace SixLabors.ImageSharp.Metadata
///
/// Gets or sets the XMP profile.
///
- internal byte[] XmpProfile { get; set; }
+ public XmpProfile XmpProfile { get; set; }
///
/// Gets or sets the list of ICC profiles.
diff --git a/src/ImageSharp/Metadata/ImageMetadata.cs b/src/ImageSharp/Metadata/ImageMetadata.cs
index 425fd9b47..39e1cef8a 100644
--- a/src/ImageSharp/Metadata/ImageMetadata.cs
+++ b/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
///
/// Initializes a new instance of the class.
///
- internal ImageMetadata()
+ public ImageMetadata()
{
this.horizontalResolution = DefaultHorizontalResolution;
this.verticalResolution = DefaultVerticalResolution;
@@ -119,13 +120,18 @@ namespace SixLabors.ImageSharp.Metadata
///
public ExifProfile ExifProfile { get; set; }
+ ///
+ /// Gets or sets the XMP profile.
+ ///
+ public XmpProfile XmpProfile { get; set; }
+
///
/// Gets or sets the list of ICC profiles.
///
public IccProfile IccProfile { get; set; }
///
- /// Gets or sets the iptc profile.
+ /// Gets or sets the IPTC profile.
///
public IptcProfile IptcProfile { get; set; }
diff --git a/src/ImageSharp/Metadata/Profiles/Exif/ExifWriter.cs b/src/ImageSharp/Metadata/Profiles/Exif/ExifWriter.cs
index 498eec952..e2ed56954 100644
--- a/src/ImageSharp/Metadata/Profiles/Exif/ExifWriter.cs
+++ b/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();
- }
-
- 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();
}
@@ -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;
}
diff --git a/src/ImageSharp/Metadata/Profiles/XMP/XmpProfile.cs b/src/ImageSharp/Metadata/Profiles/XMP/XmpProfile.cs
new file mode 100644
index 000000000..8fba243ce
--- /dev/null
+++ b/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
+{
+ ///
+ /// Represents an XMP profile, providing access to the raw XML.
+ /// See for the full specification.
+ ///
+ public sealed class XmpProfile : IDeepCloneable
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public XmpProfile()
+ : this((byte[])null)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The UTF8 encoded byte array to read the XMP profile from.
+ public XmpProfile(byte[] data) => this.Data = data;
+
+ ///
+ /// Initializes a new instance of the class
+ /// by making a copy from another XMP profile.
+ ///
+ /// The other XMP profile, from which the clone should be made from.
+ private XmpProfile(XmpProfile other)
+ {
+ Guard.NotNull(other, nameof(other));
+
+ this.Data = other.Data;
+ }
+
+ ///
+ /// Gets the XMP raw data byte array.
+ ///
+ internal byte[] Data { get; private set; }
+
+ ///
+ /// Gets the raw XML document containing the XMP profile.
+ ///
+ /// The
+ 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);
+ }
+
+ ///
+ /// Convert the content of this into a byte array.
+ ///
+ /// The
+ public byte[] ToByteArray()
+ {
+ byte[] result = new byte[this.Data.Length];
+ this.Data.AsSpan().CopyTo(result);
+ return result;
+ }
+
+ ///
+ public XmpProfile DeepClone() => new(this);
+ }
+}
diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs
index 7b3e20aa2..0ef5090cc 100644
--- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs
+++ b/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 MetadataTestData =
- new TheoryData
+ 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 RatioFiles =
- new TheoryData
+ 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 QualityFiles =
- new TheoryData
+ new()
{
{ TestImages.Jpeg.Baseline.Calliphora, 80 },
{ TestImages.Jpeg.Progressive.Fb, 75 },
diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs
index 7715ac3a3..6a47a9577 100644
--- a/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs
+++ b/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 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 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);
diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs
index a051de1c0..7fba86b4f 100644
--- a/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs
+++ b/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(TestImageProvider provider, bool ignoreMetadata)
+ where TPixel : unmanaged, IPixel
+ {
+ var decoder = new WebpDecoder { IgnoreMetadata = ignoreMetadata };
+
+ using Image 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)]
diff --git a/tests/ImageSharp.Tests/IO/BufferedReadStreamTests.cs b/tests/ImageSharp.Tests/IO/BufferedReadStreamTests.cs
index 8e7321864..f968b16f0 100644
--- a/tests/ImageSharp.Tests/IO/BufferedReadStreamTests.cs
+++ b/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(() => reader.Position = -stream.Length);
- Assert.Throws(() => 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];
diff --git a/tests/ImageSharp.Tests/Metadata/ImageFrameMetadataTests.cs b/tests/ImageSharp.Tests/Metadata/ImageFrameMetadataTests.cs
index f1a90d43e..dd8ae3d5a 100644
--- a/tests/ImageSharp.Tests/Metadata/ImageFrameMetadataTests.cs
+++ b/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));
diff --git a/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs b/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs
index 3ee20cbd1..7fc3ff6f1 100644
--- a/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs
+++ b/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());
}
+ [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 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 image = TestFile.Create(TestImages.Jpeg.Baseline.Floorplan).CreateRgba32Image();
diff --git a/tests/ImageSharp.Tests/Metadata/Profiles/XMP/XmpProfileTests.cs b/tests/ImageSharp.Tests/Metadata/Profiles/XMP/XmpProfileTests.cs
new file mode 100644
index 000000000..81dad699a
--- /dev/null
+++ b/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(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ using (Image 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(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ using (Image 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(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ using (Image 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(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ using (Image 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(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ using (Image 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(1, 1);
+ XmpProfile original = CreateMinimalXmlProfile();
+ image.Metadata.XmpProfile = original;
+ var encoder = new GifEncoder();
+
+ // act
+ using Image 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(1, 1);
+ XmpProfile original = CreateMinimalXmlProfile();
+ image.Metadata.XmpProfile = original;
+ var encoder = new JpegEncoder();
+
+ // act
+ using Image 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.File(TestImages.Jpeg.Baseline.ExtendedXmp);
+ using Image image = await provider.GetImageAsync(JpegDecoder);
+ XmpProfile original = image.Metadata.XmpProfile;
+ var encoder = new JpegEncoder();
+
+ // act
+ using Image 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(1, 1);
+ XmpProfile original = CreateMinimalXmlProfile();
+ image.Metadata.XmpProfile = original;
+ var encoder = new PngEncoder();
+
+ // act
+ using Image 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(1, 1);
+ XmpProfile original = CreateMinimalXmlProfile();
+ image.Frames.RootFrame.Metadata.XmpProfile = original;
+ var encoder = new TiffEncoder();
+
+ // act
+ using Image 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(1, 1);
+ XmpProfile original = CreateMinimalXmlProfile();
+ image.Metadata.XmpProfile = original;
+ var encoder = new WebpEncoder();
+
+ // act
+ using Image 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 = $" ";
+ byte[] data = Encoding.UTF8.GetBytes(content);
+ var profile = new XmpProfile(data);
+ return profile;
+ }
+
+ private static Image WriteAndRead(Image image, IImageEncoder encoder)
+ {
+ using (var memStream = new MemoryStream())
+ {
+ image.Save(memStream, encoder);
+ image.Dispose();
+
+ memStream.Position = 0;
+ return Image.Load(memStream);
+ }
+ }
+ }
+}
diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs
index deed0b240..0860bb5ae 100644
--- a/tests/ImageSharp.Tests/TestImages.cs
+++ b/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.
diff --git a/tests/Images/Input/Jpg/baseline/Metadata-test-file.jpg b/tests/Images/Input/Jpg/baseline/Metadata-test-file.jpg
new file mode 100644
index 000000000..160d7ebf8
--- /dev/null
+++ b/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
diff --git a/tests/Images/Input/Jpg/baseline/extended-xmp.jpg b/tests/Images/Input/Jpg/baseline/extended-xmp.jpg
new file mode 100644
index 000000000..6fc84b95e
--- /dev/null
+++ b/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
diff --git a/tests/Images/Input/Jpg/issues/issue-1932-app0-resolution.jpg b/tests/Images/Input/Jpg/issues/issue-1932-app0-resolution.jpg
new file mode 100644
index 000000000..7f14e808e
--- /dev/null
+++ b/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
diff --git a/tests/Images/Input/Png/xmp-colorpalette.png b/tests/Images/Input/Png/xmp-colorpalette.png
new file mode 100644
index 000000000..375879413
--- /dev/null
+++ b/tests/Images/Input/Png/xmp-colorpalette.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:fb55607fd7de6a47d8dd242c1a7be9627c564821554db896ed46603d15963c06
+size 1025
diff --git a/tests/Images/Input/Webp/xmp_lossy.webp b/tests/Images/Input/Webp/xmp_lossy.webp
new file mode 100644
index 000000000..4e92f280c
--- /dev/null
+++ b/tests/Images/Input/Webp/xmp_lossy.webp
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:755a63652695d7e190f375c9c0697cd37c9b601cd54405c704ec8efc200e67fc
+size 474772