mirror of https://github.com/SixLabors/ImageSharp
committed by
GitHub
47 changed files with 1443 additions and 484 deletions
@ -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 |
|||
|
|||
@ -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 |
|||
@ -0,0 +1,97 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.IO; |
|||
using SixLabors.ImageSharp.Metadata.Profiles.Xmp; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Gif |
|||
{ |
|||
internal readonly struct GifXmpApplicationExtension : IGifExtension |
|||
{ |
|||
public GifXmpApplicationExtension(byte[] data) => this.Data = data; |
|||
|
|||
public byte Label => GifConstants.ApplicationExtensionLabel; |
|||
|
|||
public int ContentLength => this.Data.Length + 269; // 12 + Data Length + 1 + 256
|
|||
|
|||
/// <summary>
|
|||
/// Gets the raw Data.
|
|||
/// </summary>
|
|||
public byte[] Data { get; } |
|||
|
|||
/// <summary>
|
|||
/// Reads the XMP metadata from the specified stream.
|
|||
/// </summary>
|
|||
/// <param name="stream">The stream to read from.</param>
|
|||
/// <returns>The XMP metadata</returns>
|
|||
/// <exception cref="ImageFormatException">Thrown if the XMP block is not properly terminated.</exception>
|
|||
public static GifXmpApplicationExtension Read(Stream stream) |
|||
{ |
|||
// Read data in blocks, until an \0 character is encountered.
|
|||
// We overshoot, indicated by the terminatorIndex variable.
|
|||
const int bufferSize = 256; |
|||
var list = new List<byte[]>(); |
|||
int terminationIndex = -1; |
|||
while (terminationIndex < 0) |
|||
{ |
|||
byte[] temp = new byte[bufferSize]; |
|||
int bytesRead = stream.Read(temp); |
|||
list.Add(temp); |
|||
terminationIndex = Array.IndexOf(temp, (byte)1); |
|||
} |
|||
|
|||
// Pack all the blocks (except magic trailer) into one single array again.
|
|||
int dataSize = ((list.Count - 1) * bufferSize) + terminationIndex; |
|||
byte[] buffer = new byte[dataSize]; |
|||
Span<byte> bufferSpan = buffer; |
|||
int pos = 0; |
|||
for (int j = 0; j < list.Count - 1; j++) |
|||
{ |
|||
list[j].CopyTo(bufferSpan.Slice(pos)); |
|||
pos += bufferSize; |
|||
} |
|||
|
|||
// Last one only needs the portion until terminationIndex copied over.
|
|||
Span<byte> lastBytes = list[list.Count - 1]; |
|||
lastBytes.Slice(0, terminationIndex).CopyTo(bufferSpan.Slice(pos)); |
|||
|
|||
// Skip the remainder of the magic trailer.
|
|||
stream.Skip(258 - (bufferSize - terminationIndex)); |
|||
return new GifXmpApplicationExtension(buffer); |
|||
} |
|||
|
|||
public int WriteTo(Span<byte> buffer) |
|||
{ |
|||
int totalSize = this.ContentLength; |
|||
if (buffer.Length < totalSize) |
|||
{ |
|||
throw new InsufficientMemoryException("Unable to write XMP metadata to GIF image"); |
|||
} |
|||
|
|||
int bytesWritten = 0; |
|||
buffer[bytesWritten++] = GifConstants.ApplicationBlockSize; |
|||
|
|||
// Write "XMP DataXMP"
|
|||
ReadOnlySpan<byte> idBytes = GifConstants.XmpApplicationIdentificationBytes; |
|||
idBytes.CopyTo(buffer.Slice(bytesWritten)); |
|||
bytesWritten += idBytes.Length; |
|||
|
|||
// XMP Data itself
|
|||
this.Data.CopyTo(buffer.Slice(bytesWritten)); |
|||
bytesWritten += this.Data.Length; |
|||
|
|||
// Write the Magic Trailer
|
|||
buffer[bytesWritten++] = 0x01; |
|||
for (byte i = 255; i > 0; i--) |
|||
{ |
|||
buffer[bytesWritten++] = i; |
|||
} |
|||
|
|||
buffer[bytesWritten++] = 0x00; |
|||
|
|||
return totalSize; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,89 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.IO; |
|||
using System.Text; |
|||
using System.Xml.Linq; |
|||
|
|||
namespace SixLabors.ImageSharp.Metadata.Profiles.Xmp |
|||
{ |
|||
/// <summary>
|
|||
/// Represents an XMP profile, providing access to the raw XML.
|
|||
/// See <seealso href="https://www.adobe.com/devnet/xmp.html"/> for the full specification.
|
|||
/// </summary>
|
|||
public sealed class XmpProfile : IDeepCloneable<XmpProfile> |
|||
{ |
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="XmpProfile"/> class.
|
|||
/// </summary>
|
|||
public XmpProfile() |
|||
: this((byte[])null) |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="XmpProfile"/> class.
|
|||
/// </summary>
|
|||
/// <param name="data">The UTF8 encoded byte array to read the XMP profile from.</param>
|
|||
public XmpProfile(byte[] data) => this.Data = data; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="XmpProfile"/> class
|
|||
/// by making a copy from another XMP profile.
|
|||
/// </summary>
|
|||
/// <param name="other">The other XMP profile, from which the clone should be made from.</param>
|
|||
private XmpProfile(XmpProfile other) |
|||
{ |
|||
Guard.NotNull(other, nameof(other)); |
|||
|
|||
this.Data = other.Data; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the XMP raw data byte array.
|
|||
/// </summary>
|
|||
internal byte[] Data { get; private set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the raw XML document containing the XMP profile.
|
|||
/// </summary>
|
|||
/// <returns>The <see cref="XDocument"/></returns>
|
|||
public XDocument GetDocument() |
|||
{ |
|||
byte[] byteArray = this.Data; |
|||
if (byteArray is null) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
// Strip leading whitespace, as the XmlReader doesn't like them.
|
|||
int count = byteArray.Length; |
|||
for (int i = count - 1; i > 0; i--) |
|||
{ |
|||
if (byteArray[i] is 0 or 0x0f) |
|||
{ |
|||
count--; |
|||
} |
|||
} |
|||
|
|||
using var stream = new MemoryStream(byteArray, 0, count); |
|||
using var reader = new StreamReader(stream, Encoding.UTF8); |
|||
return XDocument.Load(reader); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Convert the content of this <see cref="XmpProfile"/> into a byte array.
|
|||
/// </summary>
|
|||
/// <returns>The <see cref="T:byte[]"/></returns>
|
|||
public byte[] ToByteArray() |
|||
{ |
|||
byte[] result = new byte[this.Data.Length]; |
|||
this.Data.AsSpan().CopyTo(result); |
|||
return result; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public XmpProfile DeepClone() => new(this); |
|||
} |
|||
} |
|||
@ -0,0 +1,260 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.IO; |
|||
using System.Text; |
|||
using System.Xml.Linq; |
|||
using SixLabors.ImageSharp.Formats; |
|||
using SixLabors.ImageSharp.Formats.Gif; |
|||
using SixLabors.ImageSharp.Formats.Jpeg; |
|||
using SixLabors.ImageSharp.Formats.Png; |
|||
using SixLabors.ImageSharp.Formats.Tiff; |
|||
using SixLabors.ImageSharp.Formats.Webp; |
|||
using SixLabors.ImageSharp.Metadata.Profiles.Xmp; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
using Xunit; |
|||
|
|||
namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Xmp |
|||
{ |
|||
public class XmpProfileTests |
|||
{ |
|||
private static GifDecoder GifDecoder => new() { IgnoreMetadata = false }; |
|||
|
|||
private static JpegDecoder JpegDecoder => new() { IgnoreMetadata = false }; |
|||
|
|||
private static PngDecoder PngDecoder => new() { IgnoreMetadata = false }; |
|||
|
|||
private static TiffDecoder TiffDecoder => new() { IgnoreMetadata = false }; |
|||
|
|||
private static WebpDecoder WebpDecoder => new() { IgnoreMetadata = false }; |
|||
|
|||
[Theory] |
|||
[WithFile(TestImages.Gif.Receipt, PixelTypes.Rgba32)] |
|||
public async void ReadXmpMetadata_FromGif_Works<TPixel>(TestImageProvider<TPixel> provider) |
|||
where TPixel : unmanaged, IPixel<TPixel> |
|||
{ |
|||
using (Image<TPixel> image = await provider.GetImageAsync(GifDecoder)) |
|||
{ |
|||
XmpProfile actual = image.Metadata.XmpProfile ?? image.Frames.RootFrame.Metadata.XmpProfile; |
|||
XmpProfileContainsExpectedValues(actual); |
|||
} |
|||
} |
|||
|
|||
[Theory] |
|||
[WithFile(TestImages.Jpeg.Baseline.Lake, PixelTypes.Rgba32)] |
|||
[WithFile(TestImages.Jpeg.Baseline.Metadata, PixelTypes.Rgba32)] |
|||
[WithFile(TestImages.Jpeg.Baseline.ExtendedXmp, PixelTypes.Rgba32)] |
|||
public async void ReadXmpMetadata_FromJpg_Works<TPixel>(TestImageProvider<TPixel> provider) |
|||
where TPixel : unmanaged, IPixel<TPixel> |
|||
{ |
|||
using (Image<TPixel> image = await provider.GetImageAsync(JpegDecoder)) |
|||
{ |
|||
XmpProfile actual = image.Metadata.XmpProfile ?? image.Frames.RootFrame.Metadata.XmpProfile; |
|||
XmpProfileContainsExpectedValues(actual); |
|||
} |
|||
} |
|||
|
|||
[Theory] |
|||
[WithFile(TestImages.Png.XmpColorPalette, PixelTypes.Rgba32)] |
|||
public async void ReadXmpMetadata_FromPng_Works<TPixel>(TestImageProvider<TPixel> provider) |
|||
where TPixel : unmanaged, IPixel<TPixel> |
|||
{ |
|||
using (Image<TPixel> image = await provider.GetImageAsync(PngDecoder)) |
|||
{ |
|||
XmpProfile actual = image.Metadata.XmpProfile ?? image.Frames.RootFrame.Metadata.XmpProfile; |
|||
XmpProfileContainsExpectedValues(actual); |
|||
} |
|||
} |
|||
|
|||
[Theory] |
|||
[WithFile(TestImages.Tiff.SampleMetadata, PixelTypes.Rgba32)] |
|||
public async void ReadXmpMetadata_FromTiff_Works<TPixel>(TestImageProvider<TPixel> provider) |
|||
where TPixel : unmanaged, IPixel<TPixel> |
|||
{ |
|||
using (Image<TPixel> image = await provider.GetImageAsync(TiffDecoder)) |
|||
{ |
|||
XmpProfile actual = image.Metadata.XmpProfile ?? image.Frames.RootFrame.Metadata.XmpProfile; |
|||
XmpProfileContainsExpectedValues(actual); |
|||
} |
|||
} |
|||
|
|||
[Theory] |
|||
[WithFile(TestImages.Webp.Lossy.WithXmp, PixelTypes.Rgba32)] |
|||
public async void ReadXmpMetadata_FromWebp_Works<TPixel>(TestImageProvider<TPixel> provider) |
|||
where TPixel : unmanaged, IPixel<TPixel> |
|||
{ |
|||
using (Image<TPixel> image = await provider.GetImageAsync(WebpDecoder)) |
|||
{ |
|||
XmpProfile actual = image.Metadata.XmpProfile ?? image.Frames.RootFrame.Metadata.XmpProfile; |
|||
XmpProfileContainsExpectedValues(actual); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void XmpProfile_ToFromByteArray_ReturnsClone() |
|||
{ |
|||
// arrange
|
|||
XmpProfile profile = CreateMinimalXmlProfile(); |
|||
byte[] original = profile.ToByteArray(); |
|||
|
|||
// act
|
|||
byte[] actual = profile.ToByteArray(); |
|||
|
|||
// assert
|
|||
Assert.False(ReferenceEquals(original, actual)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void XmpProfile_CloneIsDeep() |
|||
{ |
|||
// arrange
|
|||
XmpProfile profile = CreateMinimalXmlProfile(); |
|||
byte[] original = profile.ToByteArray(); |
|||
|
|||
// act
|
|||
XmpProfile clone = profile.DeepClone(); |
|||
byte[] actual = clone.ToByteArray(); |
|||
|
|||
// assert
|
|||
Assert.False(ReferenceEquals(original, actual)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void WritingGif_PreservesXmpProfile() |
|||
{ |
|||
// arrange
|
|||
var image = new Image<Rgba32>(1, 1); |
|||
XmpProfile original = CreateMinimalXmlProfile(); |
|||
image.Metadata.XmpProfile = original; |
|||
var encoder = new GifEncoder(); |
|||
|
|||
// act
|
|||
using Image<Rgba32> reloadedImage = WriteAndRead(image, encoder); |
|||
|
|||
// assert
|
|||
XmpProfile actual = reloadedImage.Metadata.XmpProfile ?? reloadedImage.Frames.RootFrame.Metadata.XmpProfile; |
|||
XmpProfileContainsExpectedValues(actual); |
|||
Assert.Equal(original.Data, actual.Data); |
|||
} |
|||
|
|||
[Fact] |
|||
public void WritingJpeg_PreservesXmpProfile() |
|||
{ |
|||
// arrange
|
|||
var image = new Image<Rgba32>(1, 1); |
|||
XmpProfile original = CreateMinimalXmlProfile(); |
|||
image.Metadata.XmpProfile = original; |
|||
var encoder = new JpegEncoder(); |
|||
|
|||
// act
|
|||
using Image<Rgba32> reloadedImage = WriteAndRead(image, encoder); |
|||
|
|||
// assert
|
|||
XmpProfile actual = reloadedImage.Metadata.XmpProfile ?? reloadedImage.Frames.RootFrame.Metadata.XmpProfile; |
|||
XmpProfileContainsExpectedValues(actual); |
|||
Assert.Equal(original.Data, actual.Data); |
|||
} |
|||
|
|||
[Fact] |
|||
public async void WritingJpeg_PreservesExtendedXmpProfile() |
|||
{ |
|||
// arrange
|
|||
var provider = TestImageProvider<Rgba32>.File(TestImages.Jpeg.Baseline.ExtendedXmp); |
|||
using Image<Rgba32> image = await provider.GetImageAsync(JpegDecoder); |
|||
XmpProfile original = image.Metadata.XmpProfile; |
|||
var encoder = new JpegEncoder(); |
|||
|
|||
// act
|
|||
using Image<Rgba32> reloadedImage = WriteAndRead(image, encoder); |
|||
|
|||
// assert
|
|||
XmpProfile actual = reloadedImage.Metadata.XmpProfile ?? reloadedImage.Frames.RootFrame.Metadata.XmpProfile; |
|||
XmpProfileContainsExpectedValues(actual); |
|||
Assert.Equal(original.Data, actual.Data); |
|||
} |
|||
|
|||
[Fact] |
|||
public void WritingPng_PreservesXmpProfile() |
|||
{ |
|||
// arrange
|
|||
var image = new Image<Rgba32>(1, 1); |
|||
XmpProfile original = CreateMinimalXmlProfile(); |
|||
image.Metadata.XmpProfile = original; |
|||
var encoder = new PngEncoder(); |
|||
|
|||
// act
|
|||
using Image<Rgba32> reloadedImage = WriteAndRead(image, encoder); |
|||
|
|||
// assert
|
|||
XmpProfile actual = reloadedImage.Metadata.XmpProfile ?? reloadedImage.Frames.RootFrame.Metadata.XmpProfile; |
|||
XmpProfileContainsExpectedValues(actual); |
|||
Assert.Equal(original.Data, actual.Data); |
|||
} |
|||
|
|||
[Fact] |
|||
public void WritingTiff_PreservesXmpProfile() |
|||
{ |
|||
// arrange
|
|||
var image = new Image<Rgba32>(1, 1); |
|||
XmpProfile original = CreateMinimalXmlProfile(); |
|||
image.Frames.RootFrame.Metadata.XmpProfile = original; |
|||
var encoder = new TiffEncoder(); |
|||
|
|||
// act
|
|||
using Image<Rgba32> reloadedImage = WriteAndRead(image, encoder); |
|||
|
|||
// assert
|
|||
XmpProfile actual = reloadedImage.Metadata.XmpProfile ?? reloadedImage.Frames.RootFrame.Metadata.XmpProfile; |
|||
XmpProfileContainsExpectedValues(actual); |
|||
Assert.Equal(original.Data, actual.Data); |
|||
} |
|||
|
|||
[Fact] |
|||
public void WritingWebp_PreservesXmpProfile() |
|||
{ |
|||
// arrange
|
|||
var image = new Image<Rgba32>(1, 1); |
|||
XmpProfile original = CreateMinimalXmlProfile(); |
|||
image.Metadata.XmpProfile = original; |
|||
var encoder = new WebpEncoder(); |
|||
|
|||
// act
|
|||
using Image<Rgba32> reloadedImage = WriteAndRead(image, encoder); |
|||
|
|||
// assert
|
|||
XmpProfile actual = reloadedImage.Metadata.XmpProfile ?? reloadedImage.Frames.RootFrame.Metadata.XmpProfile; |
|||
XmpProfileContainsExpectedValues(actual); |
|||
Assert.Equal(original.Data, actual.Data); |
|||
} |
|||
|
|||
private static void XmpProfileContainsExpectedValues(XmpProfile xmp) |
|||
{ |
|||
Assert.NotNull(xmp); |
|||
XDocument document = xmp.GetDocument(); |
|||
Assert.NotNull(document); |
|||
Assert.Equal("xmpmeta", document.Root.Name.LocalName); |
|||
Assert.Equal("adobe:ns:meta/", document.Root.Name.NamespaceName); |
|||
} |
|||
|
|||
private static XmpProfile CreateMinimalXmlProfile() |
|||
{ |
|||
string content = $"<?xpacket begin='' id='{Guid.NewGuid()}'?><x:xmpmeta xmlns:x='adobe:ns:meta/'></x:xmpmeta><?xpacket end='w'?> "; |
|||
byte[] data = Encoding.UTF8.GetBytes(content); |
|||
var profile = new XmpProfile(data); |
|||
return profile; |
|||
} |
|||
|
|||
private static Image<Rgba32> WriteAndRead(Image<Rgba32> image, IImageEncoder encoder) |
|||
{ |
|||
using (var memStream = new MemoryStream()) |
|||
{ |
|||
image.Save(memStream, encoder); |
|||
image.Dispose(); |
|||
|
|||
memStream.Position = 0; |
|||
return Image.Load<Rgba32>(memStream); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:800911efb6f0c796d61b5ea14fc67fe891aaae3c04a49cfd5b86e68958598436 |
|||
size 138810 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:000c67f210059b101570949e889846bb11d6bdc801bd641d7d26424ad9cd027f |
|||
size 623986 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:3654c48003b85c1110bad8c31d2f94eaf4dcfe488698246b3ead4b54715d8d18 |
|||
size 1325 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:fb55607fd7de6a47d8dd242c1a7be9627c564821554db896ed46603d15963c06 |
|||
size 1025 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:755a63652695d7e190f375c9c0697cd37c9b601cd54405c704ec8efc200e67fc |
|||
size 474772 |
|||
Loading…
Reference in new issue